Repository navigation
fix(csharp): serialize object oneOf variants #25549
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech) | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
| */ | ||
| package org.openapitools.codegen.csharpnetcore; | ||
|
|
||
| import org.openapitools.codegen.DefaultGenerator; | ||
| import org.openapitools.codegen.config.CodegenConfigurator; | ||
| import org.testng.annotations.Test; | ||
|
|
||
| import java.nio.file.Path; | ||
|
|
||
| import static org.openapitools.codegen.TestUtils.assertFileContains; | ||
| import static org.openapitools.codegen.TestUtils.newTempFolder; | ||
|
|
||
| public class CSharpGenericHostOneOfSerializationTest { | ||
| @Test | ||
| public void testObjectOneOfWithoutDiscriminatorWritesVariant() throws Exception { | ||
| Path output = newTempFolder(); | ||
| CodegenConfigurator config = new CodegenConfigurator().setGeneratorName("csharp").setLibrary("generichost") | ||
| .setInputSpec("src/test/resources/3_0/csharp/oneof-object-serialization.yaml") | ||
| .setOutputDir(output.toString()).addAdditionalProperty("packageName", "OneOfRepro"); | ||
| new DefaultGenerator().opts(config.toClientOptInput()).generate(); | ||
| assertFileContains(output.resolve("src/OneOfRepro/Model/Entry.cs"), | ||
| "if (entry.FileEntry != null)", | ||
| "converter.WriteProperties(writer, entry.FileEntry, jsonSerializerOptions);", | ||
| "if (entry.FolderEntry != null)", | ||
| "converter.WriteProperties(writer, entry.FolderEntry, jsonSerializerOptions);"); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| openapi: 3.0.3 | ||
| info: | ||
| title: oneOf reproduction | ||
| version: 1.0.0 | ||
| paths: {} | ||
| components: | ||
| schemas: | ||
| Entry: | ||
| oneOf: | ||
| - $ref: '#/components/schemas/FileEntry' | ||
| - $ref: '#/components/schemas/FolderEntry' | ||
| FileEntry: | ||
| type: object | ||
| required: [name, size] | ||
| properties: | ||
| name: | ||
| type: string | ||
| size: | ||
| type: integer | ||
| FolderEntry: | ||
| type: object | ||
| required: [name, id] | ||
| properties: | ||
| name: | ||
| type: string | ||
| id: | ||
| type: string |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -205,6 +205,18 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt | |||||
| { | ||||||
| writer.WriteStartObject(); | ||||||
|
|
||||||
| if (fruit.Apple != null) | ||||||
| { | ||||||
| AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple))); | ||||||
| converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| if (fruit.Banana != null) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: These independent checks serialize both variants when callers set both public properties, although Prompt for AI agents
Suggested change
|
||||||
| { | ||||||
| BananaJsonConverter converter = (BananaJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Banana))); | ||||||
| converter.WriteProperties(writer, fruit.Banana, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| WriteProperties(writer, fruit, jsonSerializerOptions); | ||||||
| writer.WriteEndObject(); | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -180,6 +180,18 @@ public override void Write(Utf8JsonWriter writer, FruitReq fruitReq, JsonSeriali | |||||
| { | ||||||
| writer.WriteStartObject(); | ||||||
|
|
||||||
| if (fruitReq.AppleReq != null) | ||||||
| { | ||||||
| AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq))); | ||||||
| converter.WriteProperties(writer, fruitReq.AppleReq, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| if (fruitReq.BananaReq != null) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: These independent checks serialize both variants when callers set both public properties, producing a payload that violates the Prompt for AI agents
Suggested change
|
||||||
| { | ||||||
| BananaReqJsonConverter converter = (BananaReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(BananaReq))); | ||||||
| converter.WriteProperties(writer, fruitReq.BananaReq, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| WriteProperties(writer, fruitReq, jsonSerializerOptions); | ||||||
| writer.WriteEndObject(); | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -253,6 +253,12 @@ public override void Write(Utf8JsonWriter writer, MixedOneOfContent mixedOneOfCo | |||||
| { | ||||||
| writer.WriteStartObject(); | ||||||
|
|
||||||
| if (mixedOneOfContent.MixedSubId != null) | ||||||
| { | ||||||
| MixedSubIdJsonConverter converter = (MixedSubIdJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(MixedSubId))); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This lookup can select a user-supplied converter that supports Prompt for AI agents
Suggested change
|
||||||
| converter.WriteProperties(writer, mixedOneOfContent.MixedSubId, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| WriteProperties(writer, mixedOneOfContent, jsonSerializerOptions); | ||||||
| writer.WriteEndObject(); | ||||||
| } | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| using System.Text.Json; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Org.OpenAPITools.Client; | ||
| using Org.OpenAPITools.Model; | ||
| using Xunit; | ||
|
|
||
| namespace Org.OpenAPITools.Test.Model | ||
| { | ||
| public class OneOfSerializationRegressionTests | ||
| { | ||
| [Fact] | ||
| public void WritesActiveObjectVariantAndWrapperProperties() | ||
| { | ||
| JsonSerializerOptions? options = null; | ||
| new HostConfiguration(new ServiceCollection()).ConfigureJsonOptions(value => options = value); | ||
| Assert.NotNull(options); | ||
| Fruit fruit = JsonSerializer.Deserialize<Fruit>("{\"kind\":\"gala\",\"color\":\"red\"}", options)!; | ||
| Assert.NotNull(fruit.Apple); | ||
| using (JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(fruit, options))) | ||
| { | ||
| Assert.Equal("gala", json.RootElement.GetProperty("kind").GetString()); | ||
| Assert.Equal("red", json.RootElement.GetProperty("color").GetString()); | ||
| } | ||
|
|
||
| fruit.Apple = null; | ||
| fruit.Banana = new Banana { Count = 2 }; | ||
| using (JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(fruit, options))) | ||
| { | ||
| Assert.Equal(2, json.RootElement.GetProperty("count").GetInt32()); | ||
| Assert.Equal("red", json.RootElement.GetProperty("color").GetString()); | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -237,6 +237,24 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt | |
| { | ||
| writer.WriteStartObject(); | ||
|
|
||
| if (fruit.Apple != null) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: This serializes every non-null oneOf slot rather than enforcing one active variant. Because Prompt for AI agents |
||
| { | ||
| AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple))); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: These lookups assume that the first converter claiming each variant is the generated Prompt for AI agents |
||
| converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions); | ||
| } | ||
|
|
||
| if (fruit.Banana != null) | ||
| { | ||
| BananaJsonConverter converter = (BananaJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Banana))); | ||
| converter.WriteProperties(writer, fruit.Banana, jsonSerializerOptions); | ||
| } | ||
|
|
||
| if (fruit.Orange != null) | ||
| { | ||
| OrangeJsonConverter converter = (OrangeJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Orange))); | ||
| converter.WriteProperties(writer, fruit.Orange, jsonSerializerOptions); | ||
| } | ||
|
|
||
| WriteProperties(writer, fruit, jsonSerializerOptions); | ||
| writer.WriteEndObject(); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -207,6 +207,18 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt | |||||
| { | ||||||
| writer.WriteStartObject(); | ||||||
|
|
||||||
| if (fruit.Apple != null) | ||||||
| { | ||||||
| AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple))); | ||||||
| converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| if (fruit.Banana != null) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: These independent branches serialize both variants when callers set both public properties, producing JSON that violates Prompt for AI agents
Suggested change
|
||||||
| { | ||||||
| BananaJsonConverter converter = (BananaJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Banana))); | ||||||
| converter.WriteProperties(writer, fruit.Banana, jsonSerializerOptions); | ||||||
| } | ||||||
|
|
||||||
| WriteProperties(writer, fruit, jsonSerializerOptions); | ||||||
| writer.WriteEndObject(); | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This lookup hard-codes the declared oneOf variant type, so a derived runtime value loses properties declared by its concrete model during serialization. Dispatch
WritePropertiesthrough the runtime concrete converter, or add a polymorphic converter abstraction that preserves derived properties.Prompt for AI agents