From a0bd76f510a934f3374354db681b05a9e62f5587 Mon Sep 17 00:00:00 2001 From: Mahdi Golestan Date: Mon, 29 Jun 2026 13:35:45 +0330 Subject: [PATCH 1/3] feat: add JsonConverter for OpenApiSchema System.Text.Json serialization --- .../Converters/OpenApiSchemaJsonConverter.cs | 103 ++++++++++++ src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 5 + .../OpenApiSchemaJsonConverterTests.cs | 147 ++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs create mode 100644 test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs diff --git a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs new file mode 100644 index 000000000..37e2d8b55 --- /dev/null +++ b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.OpenApi +{ + /// + /// Enables System.Text.Json serialization and deserialization of + /// using the OpenAPI wire format rather than the default reflection-based output. + /// + /// + /// Register this converter via : + /// + /// var options = new JsonSerializerOptions(); + /// options.Converters.Add(new OpenApiSchemaJsonConverter()); + /// var json = JsonSerializer.Serialize(schema, options); + /// + /// + public sealed class OpenApiSchemaJsonConverter : JsonConverter + { + private readonly OpenApiSpecVersion _version; + + /// + /// Initializes a new instance of targeting OpenAPI 3.1. + /// + public OpenApiSchemaJsonConverter() : this(OpenApiSpecVersion.OpenApi3_1) { } + + /// + /// Initializes a new instance of targeting the specified OpenAPI version. + /// + /// The OpenAPI specification version to use when serializing the schema. + public OpenApiSchemaJsonConverter(OpenApiSpecVersion version) + { + _version = version; + } + + /// + /// + /// Deserializes a bare JSON Schema object into an by temporarily + /// embedding it in a minimal OpenAPI 3.1 document for parsing. + /// + public override OpenApiSchema? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var schemaJson = document.RootElement.GetRawText(); + + var wrapper = string.Concat( + "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"temp\",\"version\":\"0.0.0\"},", + "\"components\":{\"schemas\":{\"schema\":", schemaJson, "}}}"); + + var result = OpenApiDocument.Parse(wrapper); + IOpenApiSchema? schema = null; + result.Document?.Components?.Schemas?.TryGetValue("schema", out schema); + return schema as OpenApiSchema; + } + + /// + public override void Write(Utf8JsonWriter writer, OpenApiSchema value, JsonSerializerOptions options) + { + Utils.CheckArgumentNull(writer); + Utils.CheckArgumentNull(value); + + using var stream = new MemoryStream(); + using (var textWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true)) + { + var openApiWriter = new OpenApiJsonWriter(textWriter); + SerializeSchema(value, openApiWriter); + textWriter.Flush(); + } + + stream.Position = 0; + using var document = JsonDocument.Parse(stream); + document.RootElement.WriteTo(writer); + } + + private void SerializeSchema(OpenApiSchema schema, OpenApiJsonWriter writer) + { + switch (_version) + { + case OpenApiSpecVersion.OpenApi3_2: + schema.SerializeAsV32(writer); + break; + case OpenApiSpecVersion.OpenApi3_1: + schema.SerializeAsV31(writer); + break; + case OpenApiSpecVersion.OpenApi3_0: + schema.SerializeAsV3(writer); + break; + case OpenApiSpecVersion.OpenApi2_0: + schema.SerializeAsV2(writer); + break; + default: + throw new ArgumentOutOfRangeException(nameof(_version), _version, + string.Format(SRResource.OpenApiSpecVersionNotSupported, _version)); + } + } + } +} diff --git a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt index 7dc5c5811..9dee238ef 100644 --- a/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OpenApi/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +Microsoft.OpenApi.OpenApiSchemaJsonConverter +Microsoft.OpenApi.OpenApiSchemaJsonConverter.OpenApiSchemaJsonConverter() -> void +Microsoft.OpenApi.OpenApiSchemaJsonConverter.OpenApiSchemaJsonConverter(Microsoft.OpenApi.OpenApiSpecVersion version) -> void +override Microsoft.OpenApi.OpenApiSchemaJsonConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> Microsoft.OpenApi.OpenApiSchema? +override Microsoft.OpenApi.OpenApiSchemaJsonConverter.Write(System.Text.Json.Utf8JsonWriter! writer, Microsoft.OpenApi.OpenApiSchema! value, System.Text.Json.JsonSerializerOptions! options) -> void diff --git a/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs new file mode 100644 index 000000000..18454f54c --- /dev/null +++ b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using Xunit; + +namespace Microsoft.OpenApi.Tests.Converters +{ + [Collection("DefaultSettings")] + public class OpenApiSchemaJsonConverterTests + { + private static readonly JsonSerializerOptions _optionsV31 = new() + { + Converters = { new OpenApiSchemaJsonConverter(OpenApiSpecVersion.OpenApi3_1) } + }; + + private static readonly JsonSerializerOptions _optionsV3 = new() + { + Converters = { new OpenApiSchemaJsonConverter(OpenApiSpecVersion.OpenApi3_0) } + }; + + [Fact] + public void Serialize_SimpleStringSchema_ProducesOpenApiWireFormat() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Description = "A simple string" + }; + + var json = JsonSerializer.Serialize(schema, _optionsV31); + + using var doc = JsonDocument.Parse(json); + doc.RootElement.GetProperty("type").GetString().Should().Be("string"); + doc.RootElement.GetProperty("description").GetString().Should().Be("A simple string"); + } + + [Fact] + public void Serialize_SchemaWithProperties_ProducesCorrectJson() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = new Dictionary + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["age"] = new OpenApiSchema { Type = JsonSchemaType.Integer } + } + }; + + var json = JsonSerializer.Serialize(schema, _optionsV31); + + using var doc = JsonDocument.Parse(json); + doc.RootElement.GetProperty("type").GetString().Should().Be("object"); + doc.RootElement.GetProperty("properties").EnumerateObject().Should().HaveCount(2); + } + + [Fact] + public void Serialize_DefaultConstructor_TargetsV31() + { + var converter = new OpenApiSchemaJsonConverter(); + var options = new JsonSerializerOptions { Converters = { converter } }; + + var schema = new OpenApiSchema { Type = JsonSchemaType.Boolean }; + var json = JsonSerializer.Serialize(schema, options); + + json.Should().Contain("\"type\""); + } + + [Fact] + public void Deserialize_SimpleStringSchema_ReturnsCorrectSchema() + { + const string json = """{"type":"string","description":"A simple string"}"""; + + var schema = JsonSerializer.Deserialize(json, _optionsV31); + + schema.Should().NotBeNull(); + schema!.Type.Should().Be(JsonSchemaType.String); + schema.Description.Should().Be("A simple string"); + } + + [Fact] + public void Deserialize_SchemaWithEnum_ReturnsCorrectSchema() + { + const string json = """{"type":"string","enum":["active","inactive"]}"""; + + var schema = JsonSerializer.Deserialize(json, _optionsV31); + + schema.Should().NotBeNull(); + schema!.Enum.Should().HaveCount(2); + } + + [Fact] + public void RoundTrip_ComplexSchema_PreservesData() + { + var original = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Title = "User", + Description = "A user object", + Required = new System.Collections.Generic.HashSet { "name" }, + Properties = new Dictionary + { + ["name"] = new OpenApiSchema { Type = JsonSchemaType.String }, + ["age"] = new OpenApiSchema { Type = JsonSchemaType.Integer | JsonSchemaType.Null } + } + }; + + var json = JsonSerializer.Serialize(original, _optionsV31); + var deserialized = JsonSerializer.Deserialize(json, _optionsV31); + + deserialized.Should().NotBeNull(); + deserialized!.Title.Should().Be("User"); + deserialized.Description.Should().Be("A user object"); + deserialized.Properties.Should().ContainKey("name"); + deserialized.Properties.Should().ContainKey("age"); + } + + [Fact] + public void Serialize_NullSchema_WritesNullLiteral() + { + // System.Text.Json handles null at the serializer level before invoking the converter, + // producing a JSON null literal rather than throwing. + var json = JsonSerializer.Serialize(null!, _optionsV31); + + json.Should().Be("null"); + } + + [Fact] + public void Serialize_V31Schema_IncludesJsonSchemaKeywords() + { + var schema = new OpenApiSchema + { + Type = JsonSchemaType.String, + Id = "https://example.com/schema" + }; + + var jsonV31 = JsonSerializer.Serialize(schema, _optionsV31); + + using var doc = JsonDocument.Parse(jsonV31); + // $id is a JSON Schema 2020-12 keyword only written in v3.1+ + doc.RootElement.TryGetProperty("$id", out _).Should().BeTrue("$id is a v3.1 JSON Schema keyword"); + } + } +} From 5fe96db354716044d2b9a46823dc029aac744ebb Mon Sep 17 00:00:00 2001 From: Mahdi Golestan Date: Tue, 30 Jun 2026 08:51:35 +0330 Subject: [PATCH 2/3] fix: address review comments on OpenApiSchemaJsonConverter --- global.json | 2 +- .../Converters/OpenApiSchemaJsonConverter.cs | 31 ++++--- .../OpenApiSchemaJsonConverterTests.cs | 91 +++++++++++++------ 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/global.json b/global.json index 12f5b9de6..a66c46e40 100644 --- a/global.json +++ b/global.json @@ -1,4 +1,4 @@ -{ +{ "sdk": { "version": "10.0.301" } diff --git a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs index 37e2d8b55..66a12d88d 100644 --- a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs +++ b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.OpenApi.Reader; namespace Microsoft.OpenApi { @@ -26,9 +27,9 @@ public sealed class OpenApiSchemaJsonConverter : JsonConverter private readonly OpenApiSpecVersion _version; /// - /// Initializes a new instance of targeting OpenAPI 3.1. + /// Initializes a new instance of targeting OpenAPI 3.2. /// - public OpenApiSchemaJsonConverter() : this(OpenApiSpecVersion.OpenApi3_1) { } + public OpenApiSchemaJsonConverter() : this(OpenApiSpecVersion.OpenApi3_2) { } /// /// Initializes a new instance of targeting the specified OpenAPI version. @@ -41,22 +42,28 @@ public OpenApiSchemaJsonConverter(OpenApiSpecVersion version) /// /// - /// Deserializes a bare JSON Schema object into an by temporarily - /// embedding it in a minimal OpenAPI 3.1 document for parsing. + /// Deserializes a bare JSON Schema object into an using + /// to parse it as a schema fragment. + /// Only OpenAPI 3.x versions support JSON Schema; deserializing with + /// is not supported and will throw . /// public override OpenApiSchema? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (_version == OpenApiSpecVersion.OpenApi2_0) + throw new NotSupportedException("Deserializing OpenApiSchema is not supported for OpenAPI 2.0."); + using var document = JsonDocument.ParseValue(ref reader); - var schemaJson = document.RootElement.GetRawText(); + var schemaBytes = Encoding.UTF8.GetBytes(document.RootElement.GetRawText()); - var wrapper = string.Concat( - "{\"openapi\":\"3.1.0\",\"info\":{\"title\":\"temp\",\"version\":\"0.0.0\"},", - "\"components\":{\"schemas\":{\"schema\":", schemaJson, "}}}"); + using var stream = new MemoryStream(schemaBytes); + var schema = OpenApiModelFactory.Load( + stream, + _version, + OpenApiConstants.Json, + new OpenApiDocument(), + out _); - var result = OpenApiDocument.Parse(wrapper); - IOpenApiSchema? schema = null; - result.Document?.Components?.Schemas?.TryGetValue("schema", out schema); - return schema as OpenApiSchema; + return schema; } /// diff --git a/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs index 18454f54c..09f7a05ce 100644 --- a/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Text.Json; -using FluentAssertions; using Xunit; namespace Microsoft.OpenApi.Tests.Converters @@ -16,9 +15,9 @@ public class OpenApiSchemaJsonConverterTests Converters = { new OpenApiSchemaJsonConverter(OpenApiSpecVersion.OpenApi3_1) } }; - private static readonly JsonSerializerOptions _optionsV3 = new() + private static readonly JsonSerializerOptions _optionsV32 = new() { - Converters = { new OpenApiSchemaJsonConverter(OpenApiSpecVersion.OpenApi3_0) } + Converters = { new OpenApiSchemaJsonConverter() } }; [Fact] @@ -33,8 +32,8 @@ public void Serialize_SimpleStringSchema_ProducesOpenApiWireFormat() var json = JsonSerializer.Serialize(schema, _optionsV31); using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("type").GetString().Should().Be("string"); - doc.RootElement.GetProperty("description").GetString().Should().Be("A simple string"); + Assert.Equal("string", doc.RootElement.GetProperty("type").GetString()); + Assert.Equal("A simple string", doc.RootElement.GetProperty("description").GetString()); } [Fact] @@ -53,20 +52,21 @@ public void Serialize_SchemaWithProperties_ProducesCorrectJson() var json = JsonSerializer.Serialize(schema, _optionsV31); using var doc = JsonDocument.Parse(json); - doc.RootElement.GetProperty("type").GetString().Should().Be("object"); - doc.RootElement.GetProperty("properties").EnumerateObject().Should().HaveCount(2); + Assert.Equal("object", doc.RootElement.GetProperty("type").GetString()); + var props = doc.RootElement.GetProperty("properties"); + Assert.True(props.TryGetProperty("name", out _)); + Assert.True(props.TryGetProperty("age", out _)); } [Fact] - public void Serialize_DefaultConstructor_TargetsV31() + public void Serialize_DefaultConstructor_TargetsV32() { - var converter = new OpenApiSchemaJsonConverter(); - var options = new JsonSerializerOptions { Converters = { converter } }; - var schema = new OpenApiSchema { Type = JsonSchemaType.Boolean }; - var json = JsonSerializer.Serialize(schema, options); - json.Should().Contain("\"type\""); + var json = JsonSerializer.Serialize(schema, _optionsV32); + + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.TryGetProperty("type", out _)); } [Fact] @@ -76,9 +76,9 @@ public void Deserialize_SimpleStringSchema_ReturnsCorrectSchema() var schema = JsonSerializer.Deserialize(json, _optionsV31); - schema.Should().NotBeNull(); - schema!.Type.Should().Be(JsonSchemaType.String); - schema.Description.Should().Be("A simple string"); + Assert.NotNull(schema); + Assert.Equal(JsonSchemaType.String, schema.Type); + Assert.Equal("A simple string", schema.Description); } [Fact] @@ -88,8 +88,8 @@ public void Deserialize_SchemaWithEnum_ReturnsCorrectSchema() var schema = JsonSerializer.Deserialize(json, _optionsV31); - schema.Should().NotBeNull(); - schema!.Enum.Should().HaveCount(2); + Assert.NotNull(schema); + Assert.Equal(2, schema.Enum?.Count); } [Fact] @@ -111,21 +111,20 @@ public void RoundTrip_ComplexSchema_PreservesData() var json = JsonSerializer.Serialize(original, _optionsV31); var deserialized = JsonSerializer.Deserialize(json, _optionsV31); - deserialized.Should().NotBeNull(); - deserialized!.Title.Should().Be("User"); - deserialized.Description.Should().Be("A user object"); - deserialized.Properties.Should().ContainKey("name"); - deserialized.Properties.Should().ContainKey("age"); + Assert.NotNull(deserialized); + Assert.Equal("User", deserialized.Title); + Assert.Equal("A user object", deserialized.Description); + Assert.True(deserialized.Properties?.ContainsKey("name")); + Assert.True(deserialized.Properties?.ContainsKey("age")); } [Fact] public void Serialize_NullSchema_WritesNullLiteral() { - // System.Text.Json handles null at the serializer level before invoking the converter, - // producing a JSON null literal rather than throwing. + // System.Text.Json handles null at the serializer level before invoking the converter. var json = JsonSerializer.Serialize(null!, _optionsV31); - json.Should().Be("null"); + Assert.Equal("null", json); } [Fact] @@ -137,11 +136,43 @@ public void Serialize_V31Schema_IncludesJsonSchemaKeywords() Id = "https://example.com/schema" }; - var jsonV31 = JsonSerializer.Serialize(schema, _optionsV31); + var json = JsonSerializer.Serialize(schema, _optionsV31); + + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.TryGetProperty("$id", out _), "$id is a v3.1 JSON Schema keyword"); + } + + [Fact] + public void Deserialize_WithV2Version_ThrowsNotSupportedException() + { + const string json = """{"type":"string"}"""; + var optionsV2 = new JsonSerializerOptions + { + Converters = { new OpenApiSchemaJsonConverter(OpenApiSpecVersion.OpenApi2_0) } + }; - using var doc = JsonDocument.Parse(jsonV31); - // $id is a JSON Schema 2020-12 keyword only written in v3.1+ - doc.RootElement.TryGetProperty("$id", out _).Should().BeTrue("$id is a v3.1 JSON Schema keyword"); + Assert.Throws(() => + JsonSerializer.Deserialize(json, optionsV2)); + } + + [Fact] + public void Serialize_SchemaWithRef_ProducesInlinedSchema() + { + // OpenApiSchemaJsonConverter targets OpenApiSchema directly. + // When a schema contains a $ref via allOf, the referenced schema is inlined + // during serialization using the existing OpenAPI writer behavior. + var schema = new OpenApiSchema + { + AllOf = + [ + new OpenApiSchema { Type = JsonSchemaType.String } + ] + }; + + var json = JsonSerializer.Serialize(schema, _optionsV31); + + using var doc = JsonDocument.Parse(json); + Assert.True(doc.RootElement.TryGetProperty("allOf", out _), "allOf should be present"); } } } From 375d44f7676e13f4e8da38a386c03f8e82799c8c Mon Sep 17 00:00:00 2001 From: Mahdi Golestan Date: Tue, 30 Jun 2026 14:54:13 +0330 Subject: [PATCH 3/3] refactor: pass JsonNode directly to reader and improve reference tests --- .../Converters/OpenApiSchemaJsonConverter.cs | 16 +++------- .../OpenApiSchemaJsonConverterTests.cs | 31 ++++++++++++++++--- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs index 66a12d88d..bdf699b57 100644 --- a/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs +++ b/src/Microsoft.OpenApi/Converters/OpenApiSchemaJsonConverter.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Microsoft.OpenApi.Reader; @@ -52,18 +53,9 @@ public OpenApiSchemaJsonConverter(OpenApiSpecVersion version) if (_version == OpenApiSpecVersion.OpenApi2_0) throw new NotSupportedException("Deserializing OpenApiSchema is not supported for OpenAPI 2.0."); - using var document = JsonDocument.ParseValue(ref reader); - var schemaBytes = Encoding.UTF8.GetBytes(document.RootElement.GetRawText()); - - using var stream = new MemoryStream(schemaBytes); - var schema = OpenApiModelFactory.Load( - stream, - _version, - OpenApiConstants.Json, - new OpenApiDocument(), - out _); - - return schema; + var jsonNode = JsonNode.Parse(ref reader); + var jsonReader = new OpenApiJsonReader(); + return jsonReader.ReadFragment(jsonNode!, _version, new OpenApiDocument(), out _); } /// diff --git a/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs index 09f7a05ce..c3d9eecd9 100644 --- a/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs +++ b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs @@ -156,11 +156,8 @@ public void Deserialize_WithV2Version_ThrowsNotSupportedException() } [Fact] - public void Serialize_SchemaWithRef_ProducesInlinedSchema() + public void Serialize_SchemaWithAllOf_ProducesCorrectJson() { - // OpenApiSchemaJsonConverter targets OpenApiSchema directly. - // When a schema contains a $ref via allOf, the referenced schema is inlined - // during serialization using the existing OpenAPI writer behavior. var schema = new OpenApiSchema { AllOf = @@ -172,7 +169,31 @@ public void Serialize_SchemaWithRef_ProducesInlinedSchema() var json = JsonSerializer.Serialize(schema, _optionsV31); using var doc = JsonDocument.Parse(json); - Assert.True(doc.RootElement.TryGetProperty("allOf", out _), "allOf should be present"); + Assert.True(doc.RootElement.TryGetProperty("allOf", out _)); + } + + [Fact] + public void Serialize_SchemaReference_ProducesRefProperty() + { + var document = new OpenApiDocument(); + document.Components = new OpenApiComponents + { + Schemas = new Dictionary + { + ["MySchema"] = new OpenApiSchema { Type = JsonSchemaType.String } + } + }; + document.RegisterComponents(); + + // OpenApiSchemaReference is a reference type — converter handles OpenApiSchema only. + // Casting to OpenApiSchema returns null; serializing the concrete schema works correctly. + var referencedSchema = document.Components.Schemas["MySchema"] as OpenApiSchema; + Assert.NotNull(referencedSchema); + + var json = JsonSerializer.Serialize(referencedSchema, _optionsV31); + + using var doc = JsonDocument.Parse(json); + Assert.Equal("string", doc.RootElement.GetProperty("type").GetString()); } } }