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
6 changes: 5 additions & 1 deletion bin/configs/csharp-generichost-net10-oneOf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
generatorName: csharp
outputDir: samples/client/petstore/csharp/generichost/net10/OneOf
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneOf.yaml
templateDir: modules/openapi-generator/src/main/resources/csharp
templateDir: modules/openapi-generator/src/test/resources/3_0/csharp/oneof-runtime
additionalProperties:
packageGuid: '{321C8C3F-0156-40C1-AE42-D59761FB9B6C}'
useCompareNetObjects: true
Expand All @@ -14,3 +14,7 @@ additionalProperties:
operationParameterSorting: alphabetical
treatWarningsAsErrors: true
warningsNotAsErrors: CS0612

files:
OneOfSerializationRegressionTests.cs:
destinationFilename: src/Org.OpenAPITools.Test/Model/OneOfSerializationRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,43 @@
{{/model.discriminator}}
{{^model.discriminator}}
{{#composedSchemas}}
{{#oneOf}}
{{#-first}}
bool oneOfValueWritten = false;
{{/-first}}
{{^vendorExtensions.x-duplicated-data-type}}
{{^isPrimitiveType}}
{{^isArray}}
{{^isMap}}
if ({{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null)
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for {{model.classname}}.");
oneOfValueWritten = true;
var variant = {{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}};
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new {{baseType}}JsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

{{/isMap}}
{{/isArray}}
{{/isPrimitiveType}}
{{/vendorExtensions.x-duplicated-data-type}}
{{#-last}}
_ = oneOfValueWritten;
{{/-last}}
{{/oneOf}}
{{#anyOf}}
if ({{#lambda.joinWithAmpersand}}{{^required}}{{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}Option.IsSet {{/required}}{{#lambda.camelcase_sanitize_param}}{{model.classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}{{^required}}Option.Value{{/required}} != null{{/lambda.joinWithAmpersand}})
{{#isPrimitiveType}}
Expand Down
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)",
"JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)",
"if (entry.FolderEntry != null)",
"Multiple oneOf variants are set for Entry.");
}
}
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
@@ -0,0 +1,106 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
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());
}
}

private static Fruit CreateFruit()
{
JsonSerializerOptions? options = null;
new HostConfiguration(new ServiceCollection()).ConfigureJsonOptions(value => options = value);
return JsonSerializer.Deserialize<Fruit>("{\"kind\":\"gala\"}", options!)!;
}

[Fact]
public void RejectsMultipleVariants()
{
Fruit fruit = CreateFruit();
fruit.Banana = new Banana { Count = 2 };
var options = new JsonSerializerOptions();
options.Converters.Add(new FruitJsonConverter());
Assert.Throws<JsonException>(() => JsonSerializer.Serialize(fruit, options));
}

[Fact]
public void UsesGeneratedVariantConverterWhenNotRegistered()
{
Fruit fruit = CreateFruit();
var options = new JsonSerializerOptions();
options.Converters.Add(new FruitJsonConverter());
using JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(fruit, options));
Assert.Equal("gala", json.RootElement.GetProperty("kind").GetString());
}

[Fact]
public void UsesCustomVariantConverter()
{
Fruit fruit = CreateFruit();
var options = new JsonSerializerOptions();
options.Converters.Add(new FruitJsonConverter());
options.Converters.Add(new CustomAppleConverter());
using JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(fruit, options));
Assert.Equal("custom", json.RootElement.GetProperty("kind").GetString());
}

[Fact]
public void UsesDerivedVariantConverter()
{
Fruit fruit = CreateFruit();
fruit.Apple = new FancyApple();
var options = new JsonSerializerOptions();
options.Converters.Add(new FruitJsonConverter());
options.Converters.Add(new FancyAppleConverter());
using JsonDocument json = JsonDocument.Parse(JsonSerializer.Serialize(fruit, options));
Assert.Equal("fancy", json.RootElement.GetProperty("extra").GetString());
}

private sealed class CustomAppleConverter : JsonConverter<Apple>
{
public override Apple Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) => throw new NotSupportedException();
public override void Write(Utf8JsonWriter writer, Apple value, JsonSerializerOptions options)
{
writer.WriteStartObject(); writer.WriteString("kind", "custom"); writer.WriteEndObject();
}
}

private sealed class FancyApple : Apple { }

private sealed class FancyAppleConverter : JsonConverter<FancyApple>
{
public override FancyApple Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) => throw new NotSupportedException();
public override void Write(Utf8JsonWriter writer, FancyApple value, JsonSerializerOptions options)
{
writer.WriteStartObject(); writer.WriteString("extra", "fancy"); writer.WriteEndObject();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ public override void Write(Utf8JsonWriter writer, OneOfNullableTest oneOfNullabl
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
_ = oneOfValueWritten;
WriteProperties(writer, oneOfNullableTest, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ public override void Write(Utf8JsonWriter writer, ParentWithPluralOneOfPropertyN
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
_ = oneOfValueWritten;
WriteProperties(writer, parentWithPluralOneOfPropertyNumber, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ public override void Write(Utf8JsonWriter writer, OneOfArrayRequest oneOfArrayRe
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
_ = oneOfValueWritten;
WriteProperties(writer, oneOfArrayRequest, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,50 @@ public override void Write(Utf8JsonWriter writer, Fruit fruit, JsonSerializerOpt
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
if (fruit.Apple != null)
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for Fruit.");
oneOfValueWritten = true;
var variant = fruit.Apple;
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new AppleJsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

if (fruit.Banana != null)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for Fruit.");
oneOfValueWritten = true;
var variant = fruit.Banana;
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new BananaJsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

_ = oneOfValueWritten;
WriteProperties(writer, fruit, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,50 @@ public override void Write(Utf8JsonWriter writer, FruitReq fruitReq, JsonSeriali
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
if (fruitReq.AppleReq != null)
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for FruitReq.");
oneOfValueWritten = true;
var variant = fruitReq.AppleReq;
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new AppleReqJsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

if (fruitReq.BananaReq != null)
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for FruitReq.");
oneOfValueWritten = true;
var variant = fruitReq.BananaReq;
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new BananaReqJsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

_ = oneOfValueWritten;
WriteProperties(writer, fruitReq, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,29 @@ public override void Write(Utf8JsonWriter writer, MixedOneOfContent mixedOneOfCo
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
if (mixedOneOfContent.MixedSubId != null)
{
if (oneOfValueWritten)
throw new JsonException("Multiple oneOf variants are set for MixedOneOfContent.");
oneOfValueWritten = true;
var variant = mixedOneOfContent.MixedSubId;
var variantOptions = jsonSerializerOptions;
if (!jsonSerializerOptions.Converters.Any(c => c.CanConvert(variant.GetType())))
{
variantOptions = new JsonSerializerOptions(jsonSerializerOptions);
variantOptions.Converters.Add(new MixedSubIdJsonConverter());
}
using (JsonDocument document = JsonDocument.Parse(JsonSerializer.Serialize(variant, variant.GetType(), variantOptions)))
{
if (document.RootElement.ValueKind != JsonValueKind.Object)
throw new JsonException("The oneOf variant must serialize as an object.");
foreach (JsonProperty property in document.RootElement.EnumerateObject())
property.WriteTo(writer);
}
}

_ = oneOfValueWritten;
WriteProperties(writer, mixedOneOfContent, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ public override void Write(Utf8JsonWriter writer, OneOfString oneOfString, JsonS
{
writer.WriteStartObject();

bool oneOfValueWritten = false;
_ = oneOfValueWritten;
WriteProperties(writer, oneOfString, jsonSerializerOptions);
writer.WriteEndObject();
}
Expand Down
Loading
Loading