Skip to content

Commit

Permalink
fix(csharp): consistent request wrapper with body params vs request r…
Browse files Browse the repository at this point in the history
…eference behavior (#6348)

* Fix nulls in request wrapper with body fields
  • Loading branch information
Swimburger authored Mar 10, 2025
1 parent 6abf786 commit 75f3f7f
Show file tree
Hide file tree
Showing 187 changed files with 5,431 additions and 429 deletions.
10 changes: 10 additions & 0 deletions fern/pages/changelogs/csharp-sdk/2025-03-10.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## 1.12.0-rc18
**`(fix):`** Make the behavior between a wrapped request with body properties and normal body request consistent.
Previously, a wrapped request with body properties would not omit `null` values even if the JSON configuration is configured to omit `null` values.


**`(fix):`** Fix a bug where required properties that were `[JsonIgnore]` threw an error during serialization.


**`(feat):`** Improve performance of query string value to string conversion by relying less on `JsonSerializer` and more on `ToString()`.

38 changes: 35 additions & 3 deletions generators/csharp/codegen/src/asIs/JsonConfiguration.Template.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using global::System.Text.Json.Nodes;
using global::System.Text.Json.Serialization;
using global::System.Text.Json.Serialization.Metadata;
using CultureInfo = global::System.Globalization.CultureInfo;

namespace <%= namespace%>;

Expand Down Expand Up @@ -60,6 +61,19 @@ static JsonOptions()
throw new ArgumentOutOfRangeException();
}
}

var jsonIgnoreAttribute = propertyInfo
.AttributeProvider?.GetCustomAttributes(
typeof(JsonIgnoreAttribute),
true
)
.OfType<JsonIgnoreAttribute>()
.FirstOrDefault();

if (jsonIgnoreAttribute is not null)
{
propertyInfo.IsRequired = false;
}
}
},
},
Expand All @@ -74,7 +88,8 @@ static JsonOptions()

internal static class JsonUtils
{
internal static string Serialize<T>(T obj) => JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions);
internal static string Serialize<T>(T obj) =>
JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions);

internal static JsonElement SerializeToElement<T>(T obj) =>
JsonSerializer.SerializeToElement(obj, JsonOptions.JsonSerializerOptions);
Expand All @@ -90,8 +105,25 @@ internal static byte[] SerializeToUtf8Bytes<T>(T obj) =>

internal static string SerializeAsString<T>(T obj)
{
var json = JsonSerializer.Serialize(obj, JsonOptions.JsonSerializerOptions);
return json.Trim('"');
return obj switch
{
null => "null",
string str => str,
true => "true",
false => "false",
int i => i.ToString(CultureInfo.InvariantCulture),
long l => l.ToString(CultureInfo.InvariantCulture),
float f => f.ToString(CultureInfo.InvariantCulture),
double d => d.ToString(CultureInfo.InvariantCulture),
decimal dec => dec.ToString(CultureInfo.InvariantCulture),
short s => s.ToString(CultureInfo.InvariantCulture),
ushort u => u.ToString(CultureInfo.InvariantCulture),
uint u => u.ToString(CultureInfo.InvariantCulture),
ulong u => u.ToString(CultureInfo.InvariantCulture),
char c => c.ToString(CultureInfo.InvariantCulture),
Guid guid => guid.ToString("D"),
_ => Serialize(obj).Trim('"')
};
}

internal static T Deserialize<T>(string json) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,27 @@ export abstract class AbstractCsharpGeneratorContext<
);
}

public createAdditionalPropertiesField(): csharp.Field {
return csharp.field({
name: "AdditionalProperties",
type: this.getAdditionalPropertiesType(),
access: csharp.Access.Public,
summary: "Additional properties received from the response, if any.",
set: csharp.Access.Internal,
get: csharp.Access.Public,
initializer: csharp.codeblock((writer) =>
writer.writeNode(
csharp.dictionary({
keyType: csharp.Type.string(),
valueType: csharp.Type.reference(this.getJsonElementClassReference()),
values: undefined
})
)
),
annotations: [this.getJsonExtensionDataAttribute()]
});
}

public getProtoAnyMapperClassReference(): csharp.ClassReference {
return csharp.classReference({
namespace: this.getCoreNamespace(),
Expand Down
54 changes: 32 additions & 22 deletions generators/csharp/model/src/generateFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,42 @@ export function generateFields({
className,
context
}: {
properties: FernIr.ObjectProperty[];
properties: (FernIr.ObjectProperty | FernIr.InlinedRequestBodyProperty)[];
className: string;
context: ModelGeneratorContext;
}): csharp.Field[] {
return properties.map((property) => {
const fieldType = context.csharpTypeMapper.convert({ reference: property.valueType });
const maybeLiteralInitializer = context.getLiteralInitializerFromTypeReference({
typeReference: property.valueType
});
const fieldAttributes = [];
if (property.propertyAccess) {
fieldAttributes.push(context.createJsonAccessAttribute(property.propertyAccess));
}
return properties.map((property) => generateField({ property, className, context }));
}

export function generateField({
property,
className,
context
}: {
property: FernIr.ObjectProperty | FernIr.InlinedRequestBodyProperty;
className: string;
context: ModelGeneratorContext;
}): csharp.Field {
const fieldType = context.csharpTypeMapper.convert({ reference: property.valueType });
const maybeLiteralInitializer = context.getLiteralInitializerFromTypeReference({
typeReference: property.valueType
});
const fieldAttributes = [];
if ("propertyAccess" in property && property.propertyAccess) {
fieldAttributes.push(context.createJsonAccessAttribute(property.propertyAccess));
}

return csharp.field({
name: getPropertyName({ className, objectProperty: property.name, context }),
type: fieldType,
access: csharp.Access.Public,
get: true,
set: true,
summary: property.docs,
jsonPropertyName: property.name.wireValue,
useRequired: true,
initializer: maybeLiteralInitializer,
annotations: fieldAttributes
});
return csharp.field({
name: getPropertyName({ className, objectProperty: property.name, context }),
type: fieldType,
access: csharp.Access.Public,
get: true,
set: true,
summary: property.docs,
jsonPropertyName: property.name.wireValue,
useRequired: true,
initializer: maybeLiteralInitializer,
annotations: fieldAttributes
});
}

Expand Down
1 change: 1 addition & 0 deletions generators/csharp/model/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { generateModels } from "./generateModels";
export { generateFields, generateField } from "./generateFields";
export { generateModelTests as generateTests } from "./generateTests";
export { generateWellKnownProtobufFiles } from "./generateWellKnownProtobufFiles";
export { generateVersion } from "./generateVersion";
Expand Down
21 changes: 1 addition & 20 deletions generators/csharp/model/src/object/ObjectGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,7 @@ export class ObjectGenerator extends FileGenerator<CSharpFile, ModelCustomConfig
const properties = [...this.objectDeclaration.properties, ...(this.objectDeclaration.extendedProperties ?? [])];
class_.addFields(generateFields({ properties, className: this.classReference.name, context: this.context }));

class_.addField(
csharp.field({
name: "AdditionalProperties",
type: this.context.getAdditionalPropertiesType(),
access: csharp.Access.Public,
summary: "Additional properties received from the response, if any.",
set: csharp.Access.Internal,
get: csharp.Access.Public,
initializer: csharp.codeblock((writer) =>
writer.writeNode(
csharp.dictionary({
keyType: csharp.Type.string(),
valueType: csharp.Type.reference(this.context.getJsonElementClassReference()),
values: undefined
})
)
),
annotations: [this.context.getJsonExtensionDataAttribute()]
})
);
class_.addField(this.context.createAdditionalPropertiesField());

class_.addMethod(this.context.getToStringMethod());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,35 +242,9 @@ export class WrappedEndpointRequest extends EndpointRequest {
requestBodyReference: `${this.getParameterName()}.${this.wrapper.bodyKey.pascalCase.safeName}`
};
},
inlinedRequestBody: (inlinedRequestBody) => {
if (this.endpoint.queryParameters.length === 0 && this.endpoint.headers.length === 0) {
return {
requestBodyReference: `${this.getParameterName()}`
};
}
const allProperties = [
...inlinedRequestBody.properties,
...(inlinedRequestBody.extendedProperties ?? [])
];
const requestBody = csharp.dictionary({
keyType: csharp.Type.string(),
valueType: csharp.Type.object(),
values: {
type: "entries",
entries: allProperties.map((property) => ({
key: csharp.codeblock(`"${property.name.wireValue}"`),
value: csharp.codeblock(
`${this.getParameterName()}.${property.name.name.pascalCase.safeName}`
)
}))
}
});
inlinedRequestBody: () => {
return {
requestBodyReference: this.getRequestBodyVariableName(),
code: csharp.codeblock((writer) => {
writer.write(`var ${this.getRequestBodyVariableName()} = `);
writer.writeNodeStatement(requestBody);
})
requestBodyReference: this.getParameterName()
};
},
fileUpload: () => undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CSharpFile, FileGenerator, csharp } from "@fern-api/csharp-codegen";
import { ExampleGenerator } from "@fern-api/fern-csharp-model";
import { ExampleGenerator, generateField } from "@fern-api/fern-csharp-model";
import { RelativeFilePath, join } from "@fern-api/fs-utils";

import {
Expand Down Expand Up @@ -121,45 +121,35 @@ export class WrappedRequestGenerator extends FileGenerator<CSharpFile, SdkCustom
);
}

const addJsonAnnotations = this.endpoint.queryParameters.length === 0 && this.endpoint.headers.length === 0;

this.endpoint.requestBody?._visit({
reference: (reference) => {
const type = this.context.csharpTypeMapper.convert({ reference: reference.requestBodyType });
const useRequired = !type.isOptional();
class_.addField(
csharp.field({
name: this.wrapper.bodyKey.pascalCase.safeName,
type: this.context.csharpTypeMapper.convert({ reference: reference.requestBodyType }),
type,
access: csharp.Access.Public,
get: true,
set: true,
summary: reference.docs,
useRequired: true
useRequired,
annotations: [this.context.getJsonIgnoreAnnotation()]
})
);
},
inlinedRequestBody: (request) => {
for (const property of [...request.properties, ...(request.extendedProperties ?? [])]) {
const propertyName = property.name.name.pascalCase.safeName;
const maybeLiteralInitializer = this.context.getLiteralInitializerFromTypeReference({
typeReference: property.valueType
const field = generateField({
property,
className: this.classReference.name,
context: this.context
});
class_.addField(
csharp.field({
name: propertyName,
type: this.context.csharpTypeMapper.convert({ reference: property.valueType }),
access: csharp.Access.Public,
get: true,
set: true,
summary: property.docs,
jsonPropertyName: addJsonAnnotations ? property.name.wireValue : undefined,
useRequired: true,
initializer: maybeLiteralInitializer
})
);
class_.addField(field);

if (isProtoRequest) {
protobufProperties.push({
propertyName,
propertyName: field.name,
typeReference: property.valueType
});
}
Expand Down
13 changes: 13 additions & 0 deletions generators/csharp/sdk/versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
# The C# SDK now uses forward-compatible enums which are not compatible with the previously generated enums.
# Set `enable-forward-compatible-enums` to `false` in the configuration to generate the old enums.
# irVersion: 53
- version: 1.12.0-rc18
createdAt: "2025-03-10"
irVersion: 57
changelogEntry:
- type: fix
summary: |
Make the behavior between a wrapped request with body properties and normal body request consistent.
Previously, a wrapped request with body properties would not omit `null` values even if the JSON configuration is configured to omit `null` values.
- type: fix
summary: |
Fix a bug where required properties that were `[JsonIgnore]` threw an error during serialization.
- type: feat
summary: Improve performance of query string value to string conversion by relying less on `JsonSerializer` and more on `ToString()`.

- version: 1.12.0-rc17
createdAt: "2025-03-09"
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 75f3f7f

Please sign in to comment.