Skip to content

fix(csharp): serialize object oneOf variants - #25549

Open
AndreyVMarkelov wants to merge 1 commit into
OpenAPITools:masterfrom
AndreyVMarkelov:andreymarkelov/fix-csharp-oneof-serialization
Open

AndreyVMarkelov wants to merge 1 commit into
OpenAPITools:masterfrom
AndreyVMarkelov:andreymarkelov/fix-csharp-oneof-serialization

Conversation

@AndreyVMarkelov

@AndreyVMarkelov AndreyVMarkelov commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #25547

For C# generichost object oneOf models without a discriminator, write the active variant's properties alongside properties declared on the wrapper.

Tests

  • Added a focused generator regression test and a .NET runtime regression test using the existing oneOf sample.
  • Verified both variants in a minimal generated client round-trip; before the fix serialization returned {}.
  • Regenerated C# generichost samples and built the .NET 10 OneOf and Petstore samples.
  • Verified the full public Dropbox C# client builds and preserves file/folder metadata on serialization with this change and [BUG][C#] Generic host omits operation path when operation has server #25546's fix applied locally.

Summary by cubic

Fixes #25547: C# generichost object oneOf models without a discriminator now serialize the active variant's properties alongside the wrapper's own properties, instead of producing {}.

  • Updates JsonConverter.mustache to emit WriteProperties calls for non-primitive oneOf variants before wrapper properties.
  • Adds a generator regression test, a .NET runtime regression test, and regenerated C# generichost samples.

Written for commit c642c64. Summary will update on new commits.

View guided diff

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

23 issues found across 63 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/petstore/csharp/generichost/net8/SourceGeneration/src/Org.OpenAPITools/Model/FruitReq.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net8/SourceGeneration/src/Org.OpenAPITools/Model/FruitReq.cs:186">
P2: This lookup can select a caller-supplied `JsonConverter<AppleReq>` and then fail the cast to `AppleReqJsonConverter`, breaking serialization when custom converters are configured. Select the generated converter by concrete type instead, and apply the same fix to the BananaReq lookup.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net8/NullReferenceTypes/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net8/NullReferenceTypes/src/Org.OpenAPITools/Model/Fruit.cs:215">
P2: This independent branch can serialize both variants when callers set `Fruit.Apple` and `Fruit.Banana`, producing an invalid `oneOf` payload. Match the deserializer's Apple-first selection with `else if` (or reject multiple populated variants).</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Model/FruitReq.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Model/FruitReq.cs:183">
P2: This lookup assumes the first converter claiming `AppleReq` is the generated `AppleReqJsonConverter`, but `ConfigureJsonOptions` allows callers to add or reorder converters. A custom converter can therefore make serialization fail at this cast; select the generated converter by concrete type (and apply the same fix to the `BananaReq` lookup).</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/FruitReq.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/FruitReq.cs:189">
P2: These independent checks serialize both variants when callers set both public properties, producing a payload that violates the `oneOf` contract. Use an `else if` (or reject ambiguous state) so only one variant is emitted.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Model/MixedOneOfContent.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Model/MixedOneOfContent.cs:256">
P2: This cast assumes the first converter matching `MixedSubId` is always the generated `MixedSubIdJsonConverter`, but `ConfigureJsonOptions` permits callers to prepend custom converters. Select the generated converter by type instead of casting an arbitrary matching converter.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net4.8/Petstore/src/Org.OpenAPITools/Model/FruitReq.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net4.8/Petstore/src/Org.OpenAPITools/Model/FruitReq.cs:181">
P2: This `oneOf` serializer writes both variants when `AppleReq` and `BananaReq` are non-null, producing a payload that violates the schema and that `Read` resolves to Apple while losing Banana. Reject the dual-populated state or enforce mutually exclusive variant properties before writing.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net4.7/OneOf/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net4.7/OneOf/src/Org.OpenAPITools/Model/Fruit.cs:244">
P1: This new Banana serialization does not round-trip through the generated client: `count` makes `Fruit.Read` accept the object as an `Apple`, because `Apple.Read` ignores unknown properties and does not require `kind`. Validate each oneOf variant's required properties or otherwise select the matching variant before returning from `Fruit.Read`.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/standard2.0/Petstore/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/standard2.0/Petstore/src/Org.OpenAPITools/Model/Fruit.cs:213">
P2: These independent guards can serialize both public variant properties into one object, violating the `oneOf` contract and causing data loss on round-trip. Make the Banana branch mutually exclusive with Apple, or reject instances where both variants are set.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net10/SourceGeneration/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/SourceGeneration/src/Org.OpenAPITools/Model/Fruit.cs:216">
P2: These independent branches serialize both variants when callers set both public properties, producing JSON that violates `Fruit`'s `oneOf` contract and may be deserialized as a different variant. Make the variant branches mutually exclusive, matching the Apple-first selection used by `Read`.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs:213">
P2: This serializes both variant objects when callers set both public properties, producing a payload that violates `Fruit`'s `oneOf` contract. Make the variant branches mutually exclusive (or reject the invalid state) so serialization emits only one active variant.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/Fruit.cs:214">
P2: These independent checks serialize both variants when callers set both public properties, although `Fruit` is a `oneOf` and `Read` selects Apple first. Use `else if` for Banana or enforce mutual exclusivity in the setters.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net4.8/Petstore/src/Org.OpenAPITools/Model/MixedOneOfContent.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net4.8/Petstore/src/Org.OpenAPITools/Model/MixedOneOfContent.cs:256">
P2: This lookup can select a custom converter that supports `MixedSubId` but is not `MixedSubIdJsonConverter`, so the cast throws `InvalidCastException` for valid customized serializer options. Restrict the lookup to `MixedSubIdJsonConverter` instances before casting.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net8/NullReferenceTypes/src/Org.OpenAPITools/Model/MixedOneOfContent.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net8/NullReferenceTypes/src/Org.OpenAPITools/Model/MixedOneOfContent.cs:258">
P2: Custom JSON options can place another converter for `MixedSubId` before the generated converter, so `First(c => c.CanConvert(...))` may return a converter that cannot be cast to `MixedSubIdJsonConverter`; serialization then throws for this variant. Select the generated converter by type (with a generated-converter fallback) instead of casting the first compatible converter.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net9/FormModels/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/FormModels/src/Org.OpenAPITools/Model/Fruit.cs:209">
P2: This lookup can select a caller-provided `JsonConverter<Apple>` and then throw `InvalidCastException` because the result is cast to `AppleJsonConverter`. Restrict the lookup to the generated converter type (as with `c is AppleJsonConverter`) so custom converters cannot break wrapper serialization.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/net9/FormModels/src/Org.OpenAPITools/Model/Fruit.cs:213">
P2: The independent variant checks serialize both object variants when both public properties are set, violating the `oneOf` shape and losing the Banana state on deserialization. Use mutually exclusive variant serialization or enforce one active variant before writing.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net9/NullReferenceTypes/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net9/NullReferenceTypes/src/Org.OpenAPITools/Model/Fruit.cs:211">
P2: These lookups select by `CanConvert` but then cast to the generated converter. A custom converter registered before the generated converter will make `Fruit` serialization throw; select the generated converter by concrete type, or avoid requiring that cast for custom converters.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/MixedOneOfContent.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/MixedOneOfContent.cs:258">
P2: This lookup can select a user-supplied converter that supports `MixedSubId` but is not `MixedSubIdJsonConverter`, causing serialization to throw before writing the variant. Select the generated converter by its concrete type instead of casting the first converter that claims support.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net4.8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net4.8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs:209">
P2: This cast assumes the first converter that supports `Apple` is the generated `AppleJsonConverter`, but `ConfigureJsonOptions` allows callers to register another compatible converter first. Select the generated converter by type (and likewise for `Banana`) so custom converters do not make `Fruit` serialization fail.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/net4.8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs:213">
P2: This branch allows serialization of both oneOf variants when `Fruit.Apple` and `Fruit.Banana` are both set, producing an invalid oneOf payload and losing Banana on deserialization. Make the variant writes mutually exclusive or reject a model with multiple active variants.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net10/OneOf/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net10/OneOf/src/Org.OpenAPITools/Model/Fruit.cs:240">
P2: This serializes every non-null oneOf slot rather than enforcing one active variant. Because `Fruit` exposes public setters, setting two variants produces ambiguous JSON and round-tripping loses the later variant; reject multiple variants or serialize only the selected variant.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/net10/OneOf/src/Org.OpenAPITools/Model/Fruit.cs:242">
P2: These lookups assume that the first converter claiming each variant is the generated `*JsonConverter`, but `ConfigureJsonOptions` allows callers to reorder or replace converters. Select the generated converter by concrete type, or serialize and copy the configured variant object, instead of casting an arbitrary `CanConvert` match; apply the same fix to Banana and Orange.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/net8/OneOf/src/Org.OpenAPITools/Model/Fruit.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/net8/OneOf/src/Org.OpenAPITools/Model/Fruit.cs:242">
P2: These lookups make `FruitJsonConverter` depend on every variant converter being registered in the options. A caller that registers only the wrapper converter now fails at `First`; resolve the generated converter with a fallback (and apply the same fix to Banana and Orange) instead of requiring this undocumented registration invariant.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache:430">
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 `WriteProperties` through the runtime concrete converter, or add a polymorphic converter abstraction that preserves derived properties.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
}

if (fruit.Banana != null)

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.

P1: This new Banana serialization does not round-trip through the generated client: count makes Fruit.Read accept the object as an Apple, because Apple.Read ignores unknown properties and does not require kind. Validate each oneOf variant's required properties or otherwise select the matching variant before returning from Fruit.Read.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net4.7/OneOf/src/Org.OpenAPITools/Model/Fruit.cs, line 244:

<comment>This new Banana serialization does not round-trip through the generated client: `count` makes `Fruit.Read` accept the object as an `Apple`, because `Apple.Read` ignores unknown properties and does not require `kind`. Validate each oneOf variant's required properties or otherwise select the matching variant before returning from `Fruit.Read`.</comment>

<file context>
@@ -235,6 +235,24 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
+                converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
+            }
+
+            if (fruit.Banana != null)
+            {
+                BananaJsonConverter converter = (BananaJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Banana)));
</file context>


if (fruitReq.AppleReq != null)
{
AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));

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: This lookup can select a caller-supplied JsonConverter<AppleReq> and then fail the cast to AppleReqJsonConverter, breaking serialization when custom converters are configured. Select the generated converter by concrete type instead, and apply the same fix to the BananaReq lookup.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net8/SourceGeneration/src/Org.OpenAPITools/Model/FruitReq.cs, line 186:

<comment>This lookup can select a caller-supplied `JsonConverter<AppleReq>` and then fail the cast to `AppleReqJsonConverter`, breaking serialization when custom converters are configured. Select the generated converter by concrete type instead, and apply the same fix to the BananaReq lookup.</comment>

<file context>
@@ -181,6 +181,18 @@ public override void Write(Utf8JsonWriter writer, FruitReq fruitReq, JsonSeriali
 
+            if (fruitReq.AppleReq != null)
+            {
+                AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));
+                converter.WriteProperties(writer, fruitReq.AppleReq, jsonSerializerOptions);
+            }
</file context>
Suggested change
AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));
AppleReqJsonConverter converter = jsonSerializerOptions.Converters.OfType<AppleReqJsonConverter>().First();

converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
}

if (fruit.Banana != null)

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: This independent branch can serialize both variants when callers set Fruit.Apple and Fruit.Banana, producing an invalid oneOf payload. Match the deserializer's Apple-first selection with else if (or reject multiple populated variants).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net8/NullReferenceTypes/src/Org.OpenAPITools/Model/Fruit.cs, line 215:

<comment>This independent branch can serialize both variants when callers set `Fruit.Apple` and `Fruit.Banana`, producing an invalid `oneOf` payload. Match the deserializer's Apple-first selection with `else if` (or reject multiple populated variants).</comment>

<file context>
@@ -206,6 +206,18 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
+                converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
+            }
+
+            if (fruit.Banana != null)
+            {
+                BananaJsonConverter converter = (BananaJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Banana)));
</file context>
Suggested change
if (fruit.Banana != null)
else if (fruit.Banana != null)


if (fruitReq.AppleReq != null)
{
AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));

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: This lookup assumes the first converter claiming AppleReq is the generated AppleReqJsonConverter, but ConfigureJsonOptions allows callers to add or reorder converters. A custom converter can therefore make serialization fail at this cast; select the generated converter by concrete type (and apply the same fix to the BananaReq lookup).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net9/Petstore/src/Org.OpenAPITools/Model/FruitReq.cs, line 183:

<comment>This lookup assumes the first converter claiming `AppleReq` is the generated `AppleReqJsonConverter`, but `ConfigureJsonOptions` allows callers to add or reorder converters. A custom converter can therefore make serialization fail at this cast; select the generated converter by concrete type (and apply the same fix to the `BananaReq` lookup).</comment>

<file context>
@@ -178,6 +178,18 @@ public override void Write(Utf8JsonWriter writer, FruitReq fruitReq, JsonSeriali
 
+            if (fruitReq.AppleReq != null)
+            {
+                AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));
+                converter.WriteProperties(writer, fruitReq.AppleReq, jsonSerializerOptions);
+            }
</file context>
Suggested change
AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(AppleReq)));
AppleReqJsonConverter converter = (AppleReqJsonConverter) jsonSerializerOptions.Converters.First(c => c is AppleReqJsonConverter);

converter.WriteProperties(writer, fruitReq.AppleReq, jsonSerializerOptions);
}

if (fruitReq.BananaReq != null)

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: These independent checks serialize both variants when callers set both public properties, producing a payload that violates the oneOf contract. Use an else if (or reject ambiguous state) so only one variant is emitted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/FruitReq.cs, line 189:

<comment>These independent checks serialize both variants when callers set both public properties, producing a payload that violates the `oneOf` contract. Use an `else if` (or reject ambiguous state) so only one variant is emitted.</comment>

<file context>
@@ -180,6 +180,18 @@ public override void Write(Utf8JsonWriter writer, FruitReq fruitReq, JsonSeriali
+                converter.WriteProperties(writer, fruitReq.AppleReq, jsonSerializerOptions);
+            }
+
+            if (fruitReq.BananaReq != null)
+            {
+                BananaReqJsonConverter converter = (BananaReqJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(BananaReq)));
</file context>
Suggested change
if (fruitReq.BananaReq != null)
else if (fruitReq.BananaReq != null)


if (fruit.Apple != null)
{
AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));

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: This cast assumes the first converter that supports Apple is the generated AppleJsonConverter, but ConfigureJsonOptions allows callers to register another compatible converter first. Select the generated converter by type (and likewise for Banana) so custom converters do not make Fruit serialization fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net4.8/FormModels/src/Org.OpenAPITools/Model/Fruit.cs, line 209:

<comment>This cast assumes the first converter that supports `Apple` is the generated `AppleJsonConverter`, but `ConfigureJsonOptions` allows callers to register another compatible converter first. Select the generated converter by type (and likewise for `Banana`) so custom converters do not make `Fruit` serialization fail.</comment>

<file context>
@@ -204,6 +204,18 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
 
+            if (fruit.Apple != null)
+            {
+                AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));
+                converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
+            }
</file context>
Suggested change
AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));
AppleJsonConverter converter = jsonSerializerOptions.Converters.OfType<AppleJsonConverter>().First();


if (fruit.Apple != null)
{
AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));

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: These lookups assume that the first converter claiming each variant is the generated *JsonConverter, but ConfigureJsonOptions allows callers to reorder or replace converters. Select the generated converter by concrete type, or serialize and copy the configured variant object, instead of casting an arbitrary CanConvert match; apply the same fix to Banana and Orange.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net10/OneOf/src/Org.OpenAPITools/Model/Fruit.cs, line 242:

<comment>These lookups assume that the first converter claiming each variant is the generated `*JsonConverter`, but `ConfigureJsonOptions` allows callers to reorder or replace converters. Select the generated converter by concrete type, or serialize and copy the configured variant object, instead of casting an arbitrary `CanConvert` match; apply the same fix to Banana and Orange.</comment>

<file context>
@@ -237,6 +237,24 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
 
+            if (fruit.Apple != null)
+            {
+                AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));
+                converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
+            }
</file context>


if (fruit.Apple != null)
{
AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));

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: These lookups make FruitJsonConverter depend on every variant converter being registered in the options. A caller that registers only the wrapper converter now fails at First; resolve the generated converter with a fallback (and apply the same fix to Banana and Orange) instead of requiring this undocumented registration invariant.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net8/OneOf/src/Org.OpenAPITools/Model/Fruit.cs, line 242:

<comment>These lookups make `FruitJsonConverter` depend on every variant converter being registered in the options. A caller that registers only the wrapper converter now fails at `First`; resolve the generated converter with a fallback (and apply the same fix to Banana and Orange) instead of requiring this undocumented registration invariant.</comment>

<file context>
@@ -237,6 +237,24 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
 
+            if (fruit.Apple != null)
+            {
+                AppleJsonConverter converter = (AppleJsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof(Apple)));
+                converter.WriteProperties(writer, fruit.Apple, jsonSerializerOptions);
+            }
</file context>

{
writer.WriteStartObject();

if (fruit.Apple != null)

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: This serializes every non-null oneOf slot rather than enforcing one active variant. Because Fruit exposes public setters, setting two variants produces ambiguous JSON and round-tripping loses the later variant; reject multiple variants or serialize only the selected variant.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At samples/client/petstore/csharp/generichost/net10/OneOf/src/Org.OpenAPITools/Model/Fruit.cs, line 240:

<comment>This serializes every non-null oneOf slot rather than enforcing one active variant. Because `Fruit` exposes public setters, setting two variants produces ambiguous JSON and round-tripping loses the later variant; reject multiple variants or serialize only the selected variant.</comment>

<file context>
@@ -237,6 +237,24 @@ 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)));
</file context>

{{^isMap}}
if ({{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
{
{{baseType}}JsonConverter converter = ({{baseType}}JsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof({{baseType}})));

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: This lookup hard-codes the declared oneOf variant type, so a derived runtime value loses properties declared by its concrete model during serialization. Dispatch WriteProperties through the runtime concrete converter, or add a polymorphic converter abstraction that preserves derived properties.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache, line 430:

<comment>This lookup hard-codes the declared oneOf variant type, so a derived runtime value loses properties declared by its concrete model during serialization. Dispatch `WriteProperties` through the runtime concrete converter, or add a polymorphic converter abstraction that preserves derived properties.</comment>

<file context>
@@ -420,6 +420,22 @@
+            {{^isMap}}
+            if ({{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
+            {
+                {{baseType}}JsonConverter converter = ({{baseType}}JsonConverter) jsonSerializerOptions.Converters.First(c => c.CanConvert(typeof({{baseType}})));
+                converter.WriteProperties(writer, {{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions);
+            }
</file context>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][C#] Generic host loses object oneOf values during serialization

1 participant