Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7344f85
Implements not required nullables
Oct 23, 2025
f0445d7
Fix NullConditionalProperty for all csharp generators without break g…
Oct 24, 2025
764d4c6
Fix modelGeneric.mustache to remove extra spaces
Oct 24, 2025
bc01737
modelGeneric.mustache : fix comment
Oct 24, 2025
4938916
Fix Option file not being added to all generators
Oct 24, 2025
a8004a6
handle equatable
Oct 24, 2025
815879b
Fix equality
Oct 27, 2025
943a63c
Fix signature of api methods
Oct 27, 2025
751994a
Fix Option.mustache operator
Oct 27, 2025
dd8503c
Fix Api signature
Oct 27, 2025
09166e3
Fix Api signature
Oct 27, 2025
0c1c279
Add String? to language specific primitives to handle nullables better
Oct 28, 2025
e7e47f5
Fix tests that were using null reference types without knowing
Oct 28, 2025
0402efe
Option.mustache: Handle null reference types better
Oct 28, 2025
095e3af
modelGeneric: Fix default value assignation
Oct 28, 2025
e798ea3
Csharp: fix nullable generation
Oct 29, 2025
288b0b6
modelGeneric: fix nullable not handled on complex types
Oct 29, 2025
0bf0df2
Option.mustache: better handling of null reference types
Oct 29, 2025
698f124
modelGeneric.mustache: fix conditional serialization
Oct 29, 2025
8ad66c1
fix typo in httpclient/api.mustache
Oct 29, 2025
df4dc2a
call postProcessModelProperty on readWriteVars
Oct 29, 2025
b2173a9
Fix tests
Oct 29, 2025
0ee2527
Fix ApiClient not using the OptionJsonConverter
Oct 29, 2025
d0deb99
Remove String? from languagePrimitives
Oct 30, 2025
88fada4
Remove String? from languagePrimitives
Oct 30, 2025
283a2eb
Copy old NullConditionalParameter.mustache in generichost to make sur…
Oct 30, 2025
0ae1ad5
Fix tests
Oct 29, 2025
54613fa
Fix modelGeneric defaultValue
Oct 30, 2025
0230f9d
Fix default value
Oct 30, 2025
112639e
Fix csharp generation
Nov 3, 2025
58f28ff
Generate samples
Nov 3, 2025
24b314a
fix tests
Nov 3, 2025
b9f531f
Fix api.mustache
Nov 3, 2025
b44670a
Fix Query Api
Nov 3, 2025
b2d6c84
Fix ToString
Nov 3, 2025
348bb53
Generate samples
Nov 3, 2025
e4a4afb
Fix csharp async api deep object management
Nov 3, 2025
cf24a1a
Fix tests
Nov 3, 2025
30ef0b5
Extract getNullablePropertyType and getNullableSchemaType
Nov 4, 2025
356ac91
Clean
Nov 4, 2025
4bb52d7
Fix api_doc generation
Nov 4, 2025
e9590cd
Update api_doc.mustache
Nov 4, 2025
84b3ef5
Refactor : use getNullableSchemaType in getTypeDeclaration
Nov 4, 2025
1a11e8f
Refactor : rename getNullableSchemaType to getNullableTypeDeclaration
Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -1157,23 +1157,26 @@ private void patchParameter(CodegenModel model, CodegenParameter parameter) {
}

protected void processOperation(CodegenOperation operation) {
String[] nestedTypes = { "List", "Collection", "ICollection", "Dictionary" };
if (operation.returnProperty == null || operation.returnProperty.items == null) {
return;
}

Arrays.stream(nestedTypes).forEach(nestedType -> {
if (operation.returnProperty != null && operation.returnType.contains("<" + nestedType + ">") && operation.returnProperty.items != null) {
String nestedReturnType = operation.returnProperty.items.dataType;
operation.returnType = operation.returnType.replace("<" + nestedType + ">", "<" + nestedReturnType + ">");
String[] nestedTypes = {"List", "Collection", "ICollection", "Dictionary"};
String dataType = getNullableTypeDeclaration(operation.returnProperty.items);

for (String nestedType : nestedTypes) {
if (operation.returnType.contains("<" + nestedType + ">")) {
operation.returnType = operation.returnType.replace("<" + nestedType + ">", "<" + dataType + ">");
operation.returnProperty.dataType = operation.returnType;
operation.returnProperty.datatypeWithEnum = operation.returnType;
}

if (operation.returnProperty != null && operation.returnType.contains(", " + nestedType + ">") && operation.returnProperty.items != null) {
String nestedReturnType = operation.returnProperty.items.dataType;
operation.returnType = operation.returnType.replace(", " + nestedType + ">", ", " + nestedReturnType + ">");
if (operation.returnType.contains(", " + nestedType + ">")) {
operation.returnType = operation.returnType.replace(", " + nestedType + ">", ", " + dataType + ">");
operation.returnProperty.dataType = operation.returnType;
operation.returnProperty.datatypeWithEnum = operation.returnType;
}
});
}
}

protected void updateCodegenParameterEnumLegacy(CodegenParameter parameter, CodegenModel model) {
Expand Down Expand Up @@ -1432,12 +1435,20 @@ private String getArrayTypeDeclaration(Schema arr) {
String arrayType = typeMapping.get("array");
StringBuilder instantiationType = new StringBuilder(arrayType);
Schema<?> items = ModelUtils.getSchemaItems(arr);
String nestedType = getTypeDeclaration(items);

// TODO: We may want to differentiate here between generics and primitive arrays.
instantiationType.append("<").append(nestedType).append(">");
instantiationType.append("<").append(getNullableTypeDeclaration(items)).append(">");
return instantiationType.toString();
}

protected String getNullableTypeDeclaration(CodegenProperty property) {
return property.dataType;
}

protected String getNullableTypeDeclaration(Schema<?> items) {
return getTypeDeclaration(items);
}

@Override
public String toInstantiationType(Schema p) {
if (ModelUtils.isArraySchema(p)) {
Expand All @@ -1453,7 +1464,7 @@ public String getTypeDeclaration(Schema p) {
} else if (ModelUtils.isMapSchema(p)) {
// Should we also support maps of maps?
Schema<?> inner = ModelUtils.getAdditionalProperties(p);
return getSchemaType(p) + "<string, " + getTypeDeclaration(inner) + ">";
return getSchemaType(p) + "<string, " + getNullableTypeDeclaration(inner) + ">";
}
return super.getTypeDeclaration(p);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,20 +393,6 @@ protected void setTypeMapping() {
}
}

@Override
protected void updateCodegenParameterEnum(CodegenParameter parameter, CodegenModel model) {
if (GENERICHOST.equals(getLibrary())) {
super.updateCodegenParameterEnum(parameter, model);
return;
}

super.updateCodegenParameterEnumLegacy(parameter, model);

if (!parameter.required && parameter.vendorExtensions.get("x-csharp-value-type") != null) { //optional
parameter.dataType = parameter.dataType + "?";
}
}

@Override
public String apiDocFileFolder() {
if (GENERICHOST.equals(getLibrary())) {
Expand Down Expand Up @@ -456,16 +442,22 @@ public CodegenModel fromModel(String name, Schema model) {
}
}

// Cleanup possible duplicates. Currently, readWriteVars can contain the same property twice. May or may not be isolated to C#.
if (codegenModel != null && codegenModel.readWriteVars != null && codegenModel.readWriteVars.size() > 1) {
int length = codegenModel.readWriteVars.size() - 1;
for (int i = length; i > (length / 2); i--) {
final CodegenProperty codegenProperty = codegenModel.readWriteVars.get(i);
// If the property at current index is found earlier in the list, remove this last instance.
if (codegenModel.readWriteVars.indexOf(codegenProperty) < i) {
codegenModel.readWriteVars.remove(i);
if (codegenModel != null && codegenModel.readWriteVars != null) {
// Cleanup possible duplicates. Currently, readWriteVars can contain the same property twice. May or may not be isolated to C#.
if (codegenModel.readWriteVars.size() > 1) {
int length = codegenModel.readWriteVars.size() - 1;
for (int i = length; i > (length / 2); i--) {
final CodegenProperty codegenProperty = codegenModel.readWriteVars.get(i);
// If the property at current index is found earlier in the list, remove this last instance.
if (codegenModel.readWriteVars.indexOf(codegenProperty) < i) {
codegenModel.readWriteVars.remove(i);
}
}
}

for (CodegenProperty prop : codegenModel.readWriteVars) {
postProcessModelProperty(codegenModel, prop);
}
}

// avoid breaking changes
Expand Down Expand Up @@ -617,23 +609,6 @@ public String getNameUsingModelPropertyNaming(String name) {
}
}

@Override
public String getNullableType(Schema p, String type) {
if (GENERICHOST.equals(getLibrary())) {
return super.getNullableType(p, type);
}

if (languageSpecificPrimitives.contains(type)) {
if (isSupportNullable() && ModelUtils.isNullable(p) && this.getNullableTypes().contains(type)) {
return type + "?";
} else {
return type;
}
} else {
return null;
}
}

@Override
public CodegenType getTag() {
return CodegenType.CLIENT;
Expand All @@ -659,16 +634,18 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
postProcessEmitDefaultValue(property.vendorExtensions);

super.postProcessModelProperty(model, property);

if (!GENERICHOST.equals(getLibrary())) {
if (property.isNullable && (nullReferenceTypesFlag || (!property.isContainer && (getNullableTypes().contains(property.dataType) || property.isEnum)))) {
property.vendorExtensions.put("x-csharp-use-nullable-operator", true);
}
}
}

@Override
public void postProcessParameter(CodegenParameter parameter) {
super.postProcessParameter(parameter);
postProcessEmitDefaultValue(parameter.vendorExtensions);

if (!GENERICHOST.equals(getLibrary()) && !parameter.dataType.endsWith("?") && !parameter.required && (nullReferenceTypesFlag || this.getNullableTypes().contains(parameter.dataType))) {
parameter.dataType = parameter.dataType + "?";
}
}

public void postProcessEmitDefaultValue(Map<String, Object> vendorExtensions) {
Expand Down Expand Up @@ -994,6 +971,8 @@ public void addSupportingFiles(final String clientPackageDir, final String packa
supportingFiles.add(new SupportingFile("ExceptionFactory.mustache", clientPackageDir, "ExceptionFactory.cs"));
supportingFiles.add(new SupportingFile("OpenAPIDateConverter.mustache", clientPackageDir, "OpenAPIDateConverter.cs"));
supportingFiles.add(new SupportingFile("ClientUtils.mustache", clientPackageDir, "ClientUtils.cs"));
supportingFiles.add(new SupportingFile("Option.mustache", clientPackageDir, "Option.cs"));

if (needsCustomHttpMethod) {
supportingFiles.add(new SupportingFile("HttpMethod.mustache", clientPackageDir, "HttpMethod.cs"));
}
Expand Down Expand Up @@ -1544,9 +1523,20 @@ public String toInstantiationType(Schema schema) {
if (ModelUtils.isMapSchema(additionalProperties)) {
inner = toInstantiationType(additionalProperties);
}
if (!GENERICHOST.equals(getLibrary())) {
if (ModelUtils.isNullable(additionalProperties) && (this.nullReferenceTypesFlag || ModelUtils.isEnumSchema(additionalProperties) || getValueTypes().contains(inner)) && !inner.endsWith("?")) {
inner += "?";
}
}
return instantiationTypes.get("map") + "<String, " + inner + ">";
} else if (ModelUtils.isArraySchema(schema)) {
String inner = getSchemaType(ModelUtils.getSchemaItems(schema));
Schema<?> schemaItems = ModelUtils.getSchemaItems(schema);
String inner = getSchemaType(schemaItems);
if (!GENERICHOST.equals(getLibrary())) {
if (ModelUtils.isNullable(schemaItems) && (this.nullReferenceTypesFlag || ModelUtils.isEnumSchema(schemaItems) || getValueTypes().contains(inner)) && !inner.endsWith("?")) {
inner += "?";
}
}
return instantiationTypes.get("array") + "<" + inner + ">";
} else {
return null;
Expand Down Expand Up @@ -1714,4 +1704,24 @@ public Map<String, Object> postProcessSupportingFileData(Map<String, Object> obj
generateYAMLSpecFile(objs);
return objs;
}

protected String getNullableTypeDeclaration(CodegenProperty property) {
String dataType = super.getNullableTypeDeclaration(property);
if (!GENERICHOST.equals(getLibrary())) {
if (property.isNullable && (this.nullReferenceTypesFlag || property.isEnum || getValueTypes().contains(dataType)) && !dataType.endsWith("?")) {
dataType += "?";
}
}
return dataType;
}

protected String getNullableTypeDeclaration(Schema<?> items) {
String nestedType = super.getNullableTypeDeclaration(items);
if (!GENERICHOST.equals(getLibrary())) {
if (ModelUtils.isNullable(items) && (this.nullReferenceTypesFlag || ModelUtils.isEnumSchema(items) || getValueTypes().contains(nestedType)) && !nestedType.endsWith("?")) {
nestedType += "?";
}
}
return nestedType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ using Polly;
using {{packageName}}.Client.Auth;
{{/hasOAuthMethods}}
using {{packageName}}.{{modelPackage}};
using {{packageName}}.{{clientPackage}};

namespace {{packageName}}.Client
{
Expand All @@ -49,7 +50,8 @@ namespace {{packageName}}.Client
{
OverrideSpecifiedNames = false
}
}
}{{^useGenericHost}},
Converters = new List<JsonConverter> { new OptionConverterFactory() }{{/useGenericHost}}
};

public CustomJsonCodec(IReadableConfiguration configuration)
Expand Down Expand Up @@ -184,7 +186,8 @@ namespace {{packageName}}.Client
{
OverrideSpecifiedNames = false
}
}
}{{^useGenericHost}},
Converters = new List<JsonConverter> { new OptionConverterFactory() }{{/useGenericHost}}
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default({{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}})
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}}{{/isNullable}}
{{#vendorExtensions.x-csharp-use-nullable-operator}}?{{/vendorExtensions.x-csharp-use-nullable-operator}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#lambda.first}}{{#isNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}} {{/isNullable}}{{^required}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}} {{/required}}{{/lambda.first}}
{{#vendorExtensions.x-csharp-use-nullable-operator}}?{{/vendorExtensions.x-csharp-use-nullable-operator}}
122 changes: 122 additions & 0 deletions modules/openapi-generator/src/main/resources/csharp/Option.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// <auto-generated>
{{>partial_header}}

using System;
using Newtonsoft.Json;

{{#nrt}}
#nullable enable

{{/nrt}}

namespace {{packageName}}.{{clientPackage}}
{
public class OptionConverter<T> : JsonConverter<Option<T>>
{
public override void WriteJson(JsonWriter writer, Option<T> value, JsonSerializer serializer)
{
if (!value.IsSet)
{
writer.WriteNull();
}
else
{
serializer.Serialize(writer, value.Value);
}
}

public override Option<T> ReadJson(JsonReader reader, Type objectType, Option<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return new Option<T>();
}

T val = serializer.Deserialize<T>(reader);
return val != null ? new Option<T>(val) : new Option<T>();
}
}

public class OptionConverterFactory : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Option<>);

public override void WriteJson(JsonWriter writer, object{{nrt?}} value, JsonSerializer serializer)
{
Type innerType = value{{nrt?}}.GetType().GetGenericArguments()[0] ?? typeof(object);
var converterType = typeof(OptionConverter<>).MakeGenericType(innerType);
var converter = (JsonConverter)Activator.CreateInstance(converterType);
converter.WriteJson(writer, value, serializer);
}

public override object ReadJson(JsonReader reader, Type objectType, object{{nrt?}} existingValue, JsonSerializer serializer)
{
Type innerType = objectType.GetGenericArguments()[0];
var converterType = typeof(OptionConverter<>).MakeGenericType(innerType);
var converter = (JsonConverter)Activator.CreateInstance(converterType){{nrt!}};
return converter.ReadJson(reader, objectType, existingValue, serializer);
}
}

/// <summary>
/// A wrapper for operation parameters which are not required
/// </summary>
public struct Option<TType>
{
/// <summary>
/// The value to send to the server
/// </summary>
public TType Value { get; }

/// <summary>
/// When true the value will be sent to the server
/// </summary>
public bool IsSet { get; }

/// <summary>
/// A wrapper for operation parameters which are not required
/// </summary>
/// <param name="value"></param>
public Option(TType value)
{
IsSet = true;
Value = value;
}

{{#equatable}}
public bool Equals(Option<TType> other)
{
if (IsSet != other.IsSet) {
return false;
}
if (!IsSet) {
return true;
}
return object.Equals(Value, other.Value);
}

public static bool operator ==(Option<TType> left, Option<TType> right)
{
return left.Equals(right);
}

public static bool operator !=(Option<TType> left, Option<TType> right)
{
return !left.Equals(right);
}
{{/equatable}}

/// <summary>
/// Implicitly converts this option to the contained type
/// </summary>
/// <param name="option"></param>
public static implicit operator TType(Option<TType> option) => option.Value;

/// <summary>
/// Implicitly converts the provided value to an Option
/// </summary>
/// <param name="value"></param>
public static implicit operator Option<TType>(TType value) => new Option<TType>(value);
}
}
Loading
Loading