Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: optional types should respect original json key during serialization #114

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions languages/go/go-server/decode_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,14 +605,19 @@ func structFromJSON(data *gjson.Result, target reflect.Value, c *ValidationConte
if !fieldMeta.IsExported() {
continue
}
switch c.KeyCasing {
case KeyCasingCamelCase:
fieldName = strcase.ToLowerCamel(fieldName)
case KeyCasingPascalCase:
case KeyCasingSnakeCase:
fieldName = strcase.ToSnake(fieldName)
default:
fieldName = strcase.ToLowerCamel(fieldName)
keyTag := fieldMeta.Tag.Get("key")
if len(keyTag) > 0 {
fieldName = keyTag
} else {
switch c.KeyCasing {
case KeyCasingCamelCase:
fieldName = strcase.ToLowerCamel(fieldName)
case KeyCasingPascalCase:
case KeyCasingSnakeCase:
fieldName = strcase.ToSnake(fieldName)
default:
fieldName = strcase.ToLowerCamel(fieldName)
}
}
jsonResult := data.Get(fieldName)
enumValues := None[[]string]()
Expand Down
12 changes: 6 additions & 6 deletions languages/ts/ts-codegen/src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function tsObjectFromSchema(
rpcGenerators: context.rpcGenerators,
});
if (prop.content) subContentParts.push(prop.content);
const fieldName = validVarName(camelCase(key));
const fieldName = validVarName(camelCase(key, { normalize: true }));
fieldParts.push(
`${getJsDocComment(subSchema.metadata)}${fieldName}: ${prop.typeName},`,
);
Expand Down Expand Up @@ -143,7 +143,7 @@ export function tsObjectFromSchema(
rpcGenerators: context.rpcGenerators,
});
if (prop.content) subContentParts.push(prop.content);
const fieldName = validVarName(camelCase(key));
const fieldName = validVarName(camelCase(key, { normalize: true }));
fieldParts.push(
`${getJsDocComment(subSchema.metadata)}${fieldName}?: ${prop.typeName},`,
);
Expand All @@ -153,15 +153,15 @@ export function tsObjectFromSchema(
${prop.fromJsonTemplate(`input.${key}`, tempKey)}
}`);
if (hasKey) {
toJsonParts.push(`if (typeof input.${key} !== 'undefined') {
toJsonParts.push(`if (typeof input.${fieldName} !== 'undefined') {
json += \`,"${key}":\`;
${prop.toJsonTemplate(`input.${key}`, "json", key)}
${prop.toJsonTemplate(`input.${fieldName}`, "json", key)}
}`);
} else {
toJsonParts.push(`if (typeof input.${key} !== 'undefined') {
toJsonParts.push(`if (typeof input.${fieldName} !== 'undefined') {
if (_hasKey) json += ',';
json += '"${key}":';
${prop.toJsonTemplate(`input.${key}`, "json", key)}
${prop.toJsonTemplate(`input.${fieldName}`, "json", key)}
_hasKey = true;
}`);
}
Expand Down
250 changes: 250 additions & 0 deletions tests/clients/dart/lib/test_client.rpc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ class TestClientTestsService {
);
}

Future<ObjectWithPascalCaseKeys> sendObjectWithPascalCaseKeys(
ObjectWithPascalCaseKeys params) async {
return parsedArriRequest(
"$_baseUrl/rpcs/tests/send-object-with-pascal-case-keys",
method: HttpMethod.post,
httpClient: _httpClient,
headers: _headers,
clientVersion: _clientVersion,
params: params.toJson(),
parser: (body) => ObjectWithPascalCaseKeys.fromJsonString(body),
);
}

Future<ObjectWithSnakeCaseKeys> sendObjectWithSnakeCaseKeys(
ObjectWithSnakeCaseKeys params) async {
return parsedArriRequest(
"$_baseUrl/rpcs/tests/send-object-with-snake-case-keys",
method: HttpMethod.post,
httpClient: _httpClient,
headers: _headers,
clientVersion: _clientVersion,
params: params.toJson(),
parser: (body) => ObjectWithSnakeCaseKeys.fromJsonString(body),
);
}

Future<ObjectWithEveryOptionalType> sendPartialObject(
ObjectWithEveryOptionalType params) async {
return parsedArriRequest(
Expand Down Expand Up @@ -2734,6 +2760,230 @@ class ObjectWithEveryNullableTypeNestedArrayElementElement
}
}

class ObjectWithPascalCaseKeys implements ArriModel {
final DateTime createdAt;
final String displayName;
final String? phoneNumber;
final String? emailAddress;
final bool? isAdmin;
const ObjectWithPascalCaseKeys({
required this.createdAt,
required this.displayName,
required this.phoneNumber,
this.emailAddress,
this.isAdmin,
});

factory ObjectWithPascalCaseKeys.empty() {
return ObjectWithPascalCaseKeys(
createdAt: DateTime.now(),
displayName: "",
phoneNumber: null,
);
}

factory ObjectWithPascalCaseKeys.fromJson(Map<String, dynamic> _input_) {
final createdAt = dateTimeFromDynamic(_input_["CreatedAt"], DateTime.now());
final displayName = typeFromDynamic<String>(_input_["DisplayName"], "");
final phoneNumber = nullableTypeFromDynamic<String>(_input_["PhoneNumber"]);
final emailAddress =
nullableTypeFromDynamic<String>(_input_["EmailAddress"]);
final isAdmin = nullableTypeFromDynamic<bool>(_input_["IsAdmin"]);
return ObjectWithPascalCaseKeys(
createdAt: createdAt,
displayName: displayName,
phoneNumber: phoneNumber,
emailAddress: emailAddress,
isAdmin: isAdmin,
);
}

factory ObjectWithPascalCaseKeys.fromJsonString(String input) {
return ObjectWithPascalCaseKeys.fromJson(json.decode(input));
}

@override
Map<String, dynamic> toJson() {
final _output_ = <String, dynamic>{
"CreatedAt": createdAt.toUtc().toIso8601String(),
"DisplayName": displayName,
"PhoneNumber": phoneNumber,
};
if (emailAddress != null) _output_["EmailAddress"] = emailAddress;
if (isAdmin != null) _output_["IsAdmin"] = isAdmin;
return _output_;
}

@override
String toJsonString() {
return json.encode(toJson());
}

@override
String toUrlQueryParams() {
final _queryParts_ = <String>[];
_queryParts_.add("CreatedAt=${createdAt.toUtc().toIso8601String()}");
_queryParts_.add("DisplayName=$displayName");
_queryParts_.add("PhoneNumber=$phoneNumber");
if (emailAddress != null) _queryParts_.add("EmailAddress=$emailAddress");
if (isAdmin != null) _queryParts_.add("IsAdmin=$isAdmin");
return _queryParts_.join("&");
}

@override
ObjectWithPascalCaseKeys copyWith({
DateTime? createdAt,
String? displayName,
String? Function()? phoneNumber,
String? Function()? emailAddress,
bool? Function()? isAdmin,
}) {
return ObjectWithPascalCaseKeys(
createdAt: createdAt ?? this.createdAt,
displayName: displayName ?? this.displayName,
phoneNumber: phoneNumber != null ? phoneNumber() : this.phoneNumber,
emailAddress: emailAddress != null ? emailAddress() : this.emailAddress,
isAdmin: isAdmin != null ? isAdmin() : this.isAdmin,
);
}

@override
List<Object?> get props => [
createdAt,
displayName,
phoneNumber,
emailAddress,
isAdmin,
];

@override
bool operator ==(Object other) {
return other is ObjectWithPascalCaseKeys &&
listsAreEqual(props, other.props);
}

@override
int get hashCode => listToHashCode(props);

@override
String toString() {
return "ObjectWithPascalCaseKeys ${toJsonString()}";
}
}

class ObjectWithSnakeCaseKeys implements ArriModel {
final DateTime createdAt;
final String displayName;
final String? phoneNumber;
final String? emailAddress;
final bool? isAdmin;
const ObjectWithSnakeCaseKeys({
required this.createdAt,
required this.displayName,
required this.phoneNumber,
this.emailAddress,
this.isAdmin,
});

factory ObjectWithSnakeCaseKeys.empty() {
return ObjectWithSnakeCaseKeys(
createdAt: DateTime.now(),
displayName: "",
phoneNumber: null,
);
}

factory ObjectWithSnakeCaseKeys.fromJson(Map<String, dynamic> _input_) {
final createdAt =
dateTimeFromDynamic(_input_["created_at"], DateTime.now());
final displayName = typeFromDynamic<String>(_input_["display_name"], "");
final phoneNumber =
nullableTypeFromDynamic<String>(_input_["phone_number"]);
final emailAddress =
nullableTypeFromDynamic<String>(_input_["email_address"]);
final isAdmin = nullableTypeFromDynamic<bool>(_input_["is_admin"]);
return ObjectWithSnakeCaseKeys(
createdAt: createdAt,
displayName: displayName,
phoneNumber: phoneNumber,
emailAddress: emailAddress,
isAdmin: isAdmin,
);
}

factory ObjectWithSnakeCaseKeys.fromJsonString(String input) {
return ObjectWithSnakeCaseKeys.fromJson(json.decode(input));
}

@override
Map<String, dynamic> toJson() {
final _output_ = <String, dynamic>{
"created_at": createdAt.toUtc().toIso8601String(),
"display_name": displayName,
"phone_number": phoneNumber,
};
if (emailAddress != null) _output_["email_address"] = emailAddress;
if (isAdmin != null) _output_["is_admin"] = isAdmin;
return _output_;
}

@override
String toJsonString() {
return json.encode(toJson());
}

@override
String toUrlQueryParams() {
final _queryParts_ = <String>[];
_queryParts_.add("created_at=${createdAt.toUtc().toIso8601String()}");
_queryParts_.add("display_name=$displayName");
_queryParts_.add("phone_number=$phoneNumber");
if (emailAddress != null) _queryParts_.add("email_address=$emailAddress");
if (isAdmin != null) _queryParts_.add("is_admin=$isAdmin");
return _queryParts_.join("&");
}

@override
ObjectWithSnakeCaseKeys copyWith({
DateTime? createdAt,
String? displayName,
String? Function()? phoneNumber,
String? Function()? emailAddress,
bool? Function()? isAdmin,
}) {
return ObjectWithSnakeCaseKeys(
createdAt: createdAt ?? this.createdAt,
displayName: displayName ?? this.displayName,
phoneNumber: phoneNumber != null ? phoneNumber() : this.phoneNumber,
emailAddress: emailAddress != null ? emailAddress() : this.emailAddress,
isAdmin: isAdmin != null ? isAdmin() : this.isAdmin,
);
}

@override
List<Object?> get props => [
createdAt,
displayName,
phoneNumber,
emailAddress,
isAdmin,
];

@override
bool operator ==(Object other) {
return other is ObjectWithSnakeCaseKeys &&
listsAreEqual(props, other.props);
}

@override
int get hashCode => listToHashCode(props);

@override
String toString() {
return "ObjectWithSnakeCaseKeys ${toJsonString()}";
}
}

class ObjectWithEveryOptionalType implements ArriModel {
final dynamic any;
final bool? boolean;
Expand Down
Loading
Loading