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
new file mode 100644
index 000000000..74f869da4
--- /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.Nodes;
+using System.Text.Json.Serialization;
+using Microsoft.OpenApi.Reader;
+
+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.2.
+ ///
+ public OpenApiSchemaJsonConverter() : this(OpenApiSpecVersion.OpenApi3_2) { }
+
+ ///
+ /// 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 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.");
+
+ var jsonNode = JsonNode.Parse(ref reader)
+ ?? throw new JsonException("Failed to parse the JSON input into a valid JsonNode.");
+ var jsonReader = new OpenApiJsonReader();
+ return jsonReader.ReadFragment(jsonNode, _version, new OpenApiDocument(), out _);
+ }
+
+ ///
+ 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..980f949be
--- /dev/null
+++ b/test/Microsoft.OpenApi.Tests/Converters/OpenApiSchemaJsonConverterTests.cs
@@ -0,0 +1,203 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Collections.Generic;
+using System.Text.Json;
+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 _optionsV32 = new()
+ {
+ Converters = { new OpenApiSchemaJsonConverter() }
+ };
+
+ [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);
+ Assert.Equal("string", doc.RootElement.GetProperty("type").GetString());
+ Assert.Equal("A simple string", doc.RootElement.GetProperty("description").GetString());
+ }
+
+ [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);
+ 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_TargetsV32()
+ {
+ var schema = new OpenApiSchema { Type = JsonSchemaType.Boolean };
+
+ var json = JsonSerializer.Serialize(schema, _optionsV32);
+
+ using var doc = JsonDocument.Parse(json);
+ Assert.True(doc.RootElement.TryGetProperty("type", out _));
+ }
+
+ [Fact]
+ public void Deserialize_SimpleStringSchema_ReturnsCorrectSchema()
+ {
+ const string json = """{"type":"string","description":"A simple string"}""";
+
+ var schema = JsonSerializer.Deserialize(json, _optionsV31);
+
+ Assert.NotNull(schema);
+ Assert.Equal(JsonSchemaType.String, schema.Type);
+ Assert.Equal("A simple string", schema.Description);
+ }
+
+ [Fact]
+ public void Deserialize_SchemaWithEnum_ReturnsCorrectSchema()
+ {
+ const string json = """{"type":"string","enum":["active","inactive"]}""";
+
+ var schema = JsonSerializer.Deserialize(json, _optionsV31);
+
+ Assert.NotNull(schema);
+ Assert.Equal(2, schema.Enum?.Count);
+ }
+
+ [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);
+
+ 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.
+ var json = JsonSerializer.Serialize(null!, _optionsV31);
+
+ Assert.Equal("null", json);
+ }
+
+ [Fact]
+ public void Serialize_V31Schema_IncludesJsonSchemaKeywords()
+ {
+ var schema = new OpenApiSchema
+ {
+ Type = JsonSchemaType.String,
+ Id = "https://example.com/schema"
+ };
+
+ 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) }
+ };
+
+ Assert.Throws(() =>
+ JsonSerializer.Deserialize(json, optionsV2));
+ }
+
+ [Fact]
+ public void Serialize_SchemaWithAllOf_ProducesCorrectJson()
+ {
+ 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 _));
+ }
+
+ [Fact]
+ public void Serialize_SchemaWithInlineReference_ProducesRefInAllOf()
+ {
+ // A schema that uses an OpenApiSchemaReference inside allOf produces a $ref in the output.
+ var document = new OpenApiDocument();
+ document.Components = new OpenApiComponents
+ {
+ Schemas = new Dictionary
+ {
+ ["MySchema"] = new OpenApiSchema { Type = JsonSchemaType.String }
+ }
+ };
+ document.RegisterComponents();
+
+ var schema = new OpenApiSchema
+ {
+ AllOf = [new OpenApiSchemaReference("MySchema", document)]
+ };
+
+ var json = JsonSerializer.Serialize(schema, _optionsV31);
+
+ using var doc = JsonDocument.Parse(json);
+ Assert.True(doc.RootElement.TryGetProperty("allOf", out var allOf));
+ var firstItem = allOf.EnumerateArray().GetEnumerator();
+ Assert.True(firstItem.MoveNext());
+ Assert.True(firstItem.Current.TryGetProperty("$ref", out _), "allOf item should contain a $ref");
+ }
+ }
+}