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
Expand Up @@ -376,7 +376,41 @@

{{/children}}
{{/discriminator}}
{{^model.discriminator}}
{{#composedSchemas}}
{{#oneOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{#isPrimitiveType}}
if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
{
JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: For primitive oneOf members the new code serializes the value as a bare JSON primitive and returns before writer.WriteStartObject() is reached. This path is asymmetric with the referenced-object path it sits next to: the object path preserves the parent's sibling properties via WriteProperties, but the primitive path writes only the bare scalar and drops every sibling property (and never emits an object). For a composed model that mixes a primitive oneOf member with sibling properties (e.g. oneOf: [string, Apple] plus a color sibling), serializing when the primitive is selected silently omits the sibling data, so the output won't round-trip. Consider writing the primitive into the object (or documenting that a primitive oneOf cannot be combined with siblings).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache, line 386:

<comment>For primitive oneOf members the new code serializes the value as a bare JSON primitive and `return`s before `writer.WriteStartObject()` is reached. This path is asymmetric with the referenced-object path it sits next to: the object path preserves the parent's sibling properties via WriteProperties, but the primitive path writes only the bare scalar and drops every sibling property (and never emits an object). For a composed model that mixes a primitive oneOf member with sibling properties (e.g. `oneOf: [string, Apple]` plus a `color` sibling), serializing when the primitive is selected silently omits the sibling data, so the output won't round-trip. Consider writing the primitive into the object (or documenting that a primitive oneOf cannot be combined with siblings).</comment>

<file context>
@@ -376,7 +376,39 @@
+            {{#isPrimitiveType}}
+            if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
+            {
+                JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions);
+                return;
+            }
</file context>

return;
}

{{/isPrimitiveType}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/oneOf}}
{{/composedSchemas}}
{{/model.discriminator}}
writer.WriteStartObject();
{{^model.discriminator}}
{{#composedSchemas}}
{{#oneOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{#isModel}}
{{^isContainer}}
if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
{
{{baseType}}JsonConverter {{#lambda.camelcase_sanitize_param}}{{baseType}}JsonConverter{{/lambda.camelcase_sanitize_param}} = new {{baseType}}JsonConverter();
{{#lambda.camelcase_sanitize_param}}{{baseType}}JsonConverter{{/lambda.camelcase_sanitize_param}}.WriteProperties(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The referenced oneOf model's properties are written directly into the parent's already-open JSON object (flattened), followed by the parent's own WriteProperties. If a oneOf child schema shares a JSON property name with a sibling property on the parent (or with the discriminator/additionalProperties), the serialized output will contain duplicate keys for that name. The included Fruit sample avoids this only by luck (child keys kind/count/sweet vs parent key color); a child such as apple defining a color property alongside Fruit's color sibling would produce duplicate "color" entries, and on deserialization the parent's value silently wins because it is written last. Consider guarding against name collisions (e.g. wrapper/discriminator object or a name-overlap check) or at least documenting this flattening limitation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache, line 404:

<comment>The referenced oneOf model's properties are written directly into the parent's already-open JSON object (flattened), followed by the parent's own WriteProperties. If a oneOf child schema shares a JSON property name with a sibling property on the parent (or with the discriminator/additionalProperties), the serialized output will contain duplicate keys for that name. The included Fruit sample avoids this only by luck (child keys kind/count/sweet vs parent key color); a child such as `apple` defining a `color` property alongside Fruit's `color` sibling would produce duplicate "color" entries, and on deserialization the parent's value silently wins because it is written last. Consider guarding against name collisions (e.g. wrapper/discriminator object or a name-overlap check) or at least documenting this flattening limitation.</comment>

<file context>
@@ -376,7 +376,39 @@
+            if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
+            {
+                {{baseType}}JsonConverter {{#lambda.camelcase_sanitize_param}}{{baseType}}JsonConverter{{/lambda.camelcase_sanitize_param}} = ({{baseType}}JsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}.GetType()));
+                {{#lambda.camelcase_sanitize_param}}{{baseType}}JsonConverter{{/lambda.camelcase_sanitize_param}}.WriteProperties(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions);
+            }
+
</file context>

}

{{/isContainer}}
{{/isModel}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/oneOf}}
{{/composedSchemas}}
{{/model.discriminator}}

{{#model.discriminator}}
{{#model.hasDiscriminatorWithNonEmptyMapping}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using Org.OpenAPITools.Model;
using Org.OpenAPITools.Client;
using System.Reflection;
using System.Text.Json;

namespace Org.OpenAPITools.Test.Model
{
Expand Down Expand Up @@ -61,5 +62,40 @@ public void ColorTest()
{
// TODO unit test for the property 'Color'
}

[Fact]
public void SerializeOneOfReferencedObject()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new AppleJsonConverter());
options.Converters.Add(new BananaJsonConverter());
options.Converters.Add(new FruitJsonConverter());
options.Converters.Add(new OrangeJsonConverter());

Fruit fruit = new Fruit(new Apple("red"));

string json = JsonSerializer.Serialize(fruit, options);

Assert.Equal("{\"kind\":\"red\"}", json);
}

[Fact]
public void SerializeOneOfReferencedObjectWithSiblingProperty()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new AppleJsonConverter());
options.Converters.Add(new BananaJsonConverter());
options.Converters.Add(new FruitJsonConverter());
options.Converters.Add(new OrangeJsonConverter());

Fruit fruit = new Fruit(
new Apple("red"),
new Option<string?>("green")
);

string json = JsonSerializer.Serialize(fruit, options);

Assert.Equal("{\"kind\":\"red\",\"color\":\"green\"}", json);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,23 @@ public override Fruit Read(ref Utf8JsonReader utf8JsonReader, Type typeToConvert
public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOptions jsonSerializerOptions)
{
writer.WriteStartObject();
if (fruit.Apple != null)
{
AppleJsonConverter appleJsonConverter = new AppleJsonConverter();
appleJsonConverter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
}

if (fruit.Banana != null)
{
BananaJsonConverter bananaJsonConverter = new BananaJsonConverter();
bananaJsonConverter.WriteProperties(writer, fruit.Banana, jsonSerializerOptions);
}

if (fruit.Orange != null)
{
OrangeJsonConverter orangeJsonConverter = new OrangeJsonConverter();
orangeJsonConverter.WriteProperties(writer, fruit.Orange, jsonSerializerOptions);
}

WriteProperties(writer, fruit, jsonSerializerOptions);
writer.WriteEndObject();
Expand Down
Loading