Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
{{>partial_header}}

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using {{packageName}}.Client;

namespace {{packageName}}.{{modelPackage}}
{
Expand All @@ -19,13 +21,8 @@ namespace {{packageName}}.{{modelPackage}}
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
MissingMemberHandling = MissingMemberHandling.Error,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand All @@ -36,13 +33,8 @@ namespace {{packageName}}.{{modelPackage}}
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
MissingMemberHandling = MissingMemberHandling.Ignore,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand Down
121 changes: 107 additions & 14 deletions modules/openapi-generator/src/main/resources/csharp/ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ using System.Threading.Tasks;
using System.Web;
{{/net60OrLater}}
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using RestSharp;
using RestSharp.Serializers;
Expand All @@ -33,6 +34,108 @@ using {{packageName}}.{{modelPackage}};

namespace {{packageName}}.Client
{
/// <summary>
/// Custom contract resolver that allows deserialization of properties with private setters.
/// This is needed because the OpenAPI generator marks read-only properties with private setters,
/// but Newtonsoft.Json's DefaultContractResolver does not populate them by default.
/// </summary>
internal class NonPublicSetterContractResolver : DefaultContractResolver
{
public NonPublicSetterContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
};
}

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);

if (!prop.Writable)
{
var property = member as System.Reflection.PropertyInfo;
if (property != null)
{
prop.Writable = property.GetSetMethod(true) != null;
}
}

return prop;
}
}

/// <summary>
/// Converts Object-typed properties from JObject/JArray to native .NET Dictionary/List
/// so that PowerShell and other consumers can access nested values directly.
/// </summary>
internal class ObjectToNativeDictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(object);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartObject)
{
var jObj = JObject.Load(reader);
return ConvertJToken(jObj);
}
else if (reader.TokenType == JsonToken.StartArray)
{
var jArr = JArray.Load(reader);
return ConvertJToken(jArr);
}
else if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
// Primitive value
return serializer.Deserialize(reader);
}
}

private object ConvertJToken(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
var dict = new Dictionary<string, object>();
foreach (var prop in ((JObject)token).Properties())
{
dict[prop.Name] = ConvertJToken(prop.Value);
}
return dict;
case JTokenType.Array:
var list = new List<object>();
foreach (var item in (JArray)token)
{
list.Add(ConvertJToken(item));
}
// Flatten single-element arrays to scalar for cleaner display
if (list.Count == 1)
{
return list[0];
}
return list;
default:
return ((JValue)token).Value;
}
}

public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("ObjectToNativeDictionaryConverter is read-only.");
}
}

/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
Expand All @@ -43,13 +146,8 @@ namespace {{packageName}}.Client
{
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

public CustomJsonCodec(IReadableConfiguration configuration)
Expand Down Expand Up @@ -178,13 +276,8 @@ namespace {{packageName}}.Client
{
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using RestSharp;
using RestSharp.Serializers;
Expand All @@ -34,6 +35,108 @@

namespace Org.OpenAPITools.Client
{
/// <summary>
/// Custom contract resolver that allows deserialization of properties with private setters.
/// This is needed because the OpenAPI generator marks read-only properties with private setters,
/// but Newtonsoft.Json's DefaultContractResolver does not populate them by default.
/// </summary>
internal class NonPublicSetterContractResolver : DefaultContractResolver
{
public NonPublicSetterContractResolver()
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
};
}

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);

if (!prop.Writable)
{
var property = member as System.Reflection.PropertyInfo;
if (property != null)
{
prop.Writable = property.GetSetMethod(true) != null;
}
}

return prop;
}
}

/// <summary>
/// Converts Object-typed properties from JObject/JArray to native .NET Dictionary/List
/// so that PowerShell and other consumers can access nested values directly.
/// </summary>
internal class ObjectToNativeDictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(object);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartObject)
{
var jObj = JObject.Load(reader);
return ConvertJToken(jObj);
}
else if (reader.TokenType == JsonToken.StartArray)
{
var jArr = JArray.Load(reader);
return ConvertJToken(jArr);
}
else if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
// Primitive value
return serializer.Deserialize(reader);
}
}

private object ConvertJToken(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
var dict = new Dictionary<string, object>();
foreach (var prop in ((JObject)token).Properties())
{
dict[prop.Name] = ConvertJToken(prop.Value);
}
return dict;
case JTokenType.Array:
var list = new List<object>();
foreach (var item in (JArray)token)
{
list.Add(ConvertJToken(item));
}
// Flatten single-element arrays to scalar for cleaner display
if (list.Count == 1)
{
return list[0];
}
return list;
default:
return ((JValue)token).Value;
}
}

public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("ObjectToNativeDictionaryConverter is read-only.");
}
}

/// <summary>
/// Allows RestSharp to Serialize/Deserialize JSON using our custom logic, but only when ContentType is JSON.
/// </summary>
Expand All @@ -44,13 +147,8 @@ internal class CustomJsonCodec : IRestSerializer, ISerializer, IDeserializer
{
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

public CustomJsonCodec(IReadableConfiguration configuration)
Expand Down Expand Up @@ -178,13 +276,8 @@ public partial class ApiClient : ISynchronousClient, IAsynchronousClient
{
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@


using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Org.OpenAPITools.Client;

namespace Org.OpenAPITools.Model
{
Expand All @@ -28,13 +30,8 @@ public abstract partial class AbstractOpenAPISchema
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
MissingMemberHandling = MissingMemberHandling.Error,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand All @@ -45,13 +42,8 @@ public abstract partial class AbstractOpenAPISchema
// OpenAPI generated types generally hide default constructors.
ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
MissingMemberHandling = MissingMemberHandling.Ignore,
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
OverrideSpecifiedNames = false
}
}
ContractResolver = new NonPublicSetterContractResolver(),
Converters = new List<JsonConverter> { new ObjectToNativeDictionaryConverter() }
};

/// <summary>
Expand Down
Loading
Loading