Skip to content
This repository was archived by the owner on Nov 11, 2025. It is now read-only.

Commit 6ed161f

Browse files
authored
Merge pull request #22 from BinkyLabs/chore/20-add-serializationdeserialization-infrastructure-for-v32
Adds serialization/deserialization unit tests for 3.2
2 parents d4f9a4f + 263a8d9 commit 6ed161f

File tree

62 files changed

+4108
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+4108
-1
lines changed

src/Microsoft.OpenApi/Models/OpenApiSchema.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
389389
{
390390
writer.WriteStartObject();
391391

392-
if (version == OpenApiSpecVersion.OpenApi3_1)
392+
if (version >= OpenApiSpecVersion.OpenApi3_1)
393393
{
394394
WriteJsonSchemaKeywords(writer);
395395
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Text.Json.Nodes;
2+
using Microsoft.OpenApi.Reader;
3+
using Microsoft.OpenApi.Reader.V32;
4+
using Xunit;
5+
6+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests;
7+
8+
public class OpenApiCallbackReferenceDeserializerTests
9+
{
10+
[Fact]
11+
public void ShouldDeserializeCallbackReferenceAnnotations()
12+
{
13+
var json =
14+
"""
15+
{
16+
"$ref": "#/components/callbacks/MyCallback"
17+
}
18+
""";
19+
20+
var hostDocument = new OpenApiDocument();
21+
hostDocument.AddComponent("MyCallback", new OpenApiCallback
22+
{
23+
// Optionally add a PathItem or similar here if needed
24+
});
25+
var jsonNode = JsonNode.Parse(json);
26+
var parseNode = ParseNode.Create(new ParsingContext(new()), jsonNode);
27+
28+
var result = OpenApiV32Deserializer.LoadCallback(parseNode, hostDocument);
29+
30+
Assert.NotNull(result);
31+
var resultReference = Assert.IsType<OpenApiCallbackReference>(result);
32+
33+
Assert.Equal("MyCallback", resultReference.Reference.Id);
34+
Assert.NotNull(resultReference.Target);
35+
}
36+
}
37+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
4+
using Microsoft.OpenApi.Reader;
5+
using Xunit;
6+
7+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests
8+
{
9+
public class OpenApiComponentsTests
10+
{
11+
[Theory]
12+
[InlineData("./FirstLevel/SecondLevel/ThridLevel/File.json#/components/schemas/ExternalRelativePathModel", "ExternalRelativePathModel", "./FirstLevel/SecondLevel/ThridLevel/File.json")]
13+
[InlineData("File.json#/components/schemas/ExternalSimpleRelativePathModel", "ExternalSimpleRelativePathModel", "File.json")]
14+
[InlineData("A:\\Dir\\File.json#/components/schemas/ExternalAbsWindowsPathModel", "ExternalAbsWindowsPathModel", "A:\\Dir\\File.json")]
15+
[InlineData("/Dir/File.json#/components/schemas/ExternalAbsUnixPathModel", "ExternalAbsUnixPathModel", "/Dir/File.json")]
16+
[InlineData("https://host.lan:1234/path/to/file/resource.json#/components/schemas/ExternalHttpsModel", "ExternalHttpsModel", "https://host.lan:1234/path/to/file/resource.json")]
17+
[InlineData("File.json", "File.json", null)]
18+
public void ParseExternalSchemaReferenceShouldSucceed(string reference, string referenceId, string externalResource)
19+
{
20+
var input = $@"{{
21+
""schemas"": {{
22+
""Model"": {{
23+
""$ref"": ""{reference.Replace("\\", "\\\\")}""
24+
}}
25+
}}
26+
}}
27+
";
28+
var openApiDocument = new OpenApiDocument();
29+
30+
// Act
31+
var components = OpenApiModelFactory.Parse<OpenApiComponents>(input, OpenApiSpecVersion.OpenApi3_2, openApiDocument, out _, "json");
32+
33+
// Assert
34+
var schema = components.Schemas["Model"] as OpenApiSchemaReference;
35+
var expected = new OpenApiSchemaReference(referenceId, openApiDocument, externalResource);
36+
Assert.Equivalent(expected, schema);
37+
}
38+
}
39+
}
40+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Net.Http;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
using System.Text.Json.Serialization;
8+
using System.Threading.Tasks;
9+
using Xunit;
10+
11+
namespace Microsoft.OpenApi.Readers.Tests.V32Tests
12+
{
13+
public class OpenApiDocumentSerializationTests
14+
{
15+
private const string SampleFolderPath = "V32Tests/Samples/OpenApiDocument/";
16+
17+
[Theory]
18+
[InlineData(OpenApiSpecVersion.OpenApi3_2)]
19+
[InlineData(OpenApiSpecVersion.OpenApi3_1)]
20+
[InlineData(OpenApiSpecVersion.OpenApi3_0)]
21+
[InlineData(OpenApiSpecVersion.OpenApi2_0)]
22+
public async Task Serialize_DoesNotMutateDom(OpenApiSpecVersion version)
23+
{
24+
// Arrange
25+
var filePath = Path.Combine(SampleFolderPath, "docWith32properties.json");
26+
var (doc, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);
27+
28+
// Act: Serialize using System.Text.Json
29+
var options = new JsonSerializerOptions
30+
{
31+
Converters =
32+
{
33+
new HttpMethodOperationDictionaryConverter()
34+
},
35+
};
36+
var originalSerialized = JsonSerializer.Serialize(doc, options);
37+
Assert.NotNull(originalSerialized); // sanity check
38+
39+
// Serialize using native OpenAPI writer
40+
var jsonWriter = new StringWriter();
41+
var openApiWriter = new OpenApiJsonWriter(jsonWriter);
42+
switch (version)
43+
{
44+
case OpenApiSpecVersion.OpenApi3_2:
45+
doc.SerializeAsV32(openApiWriter);
46+
break;
47+
case OpenApiSpecVersion.OpenApi3_1:
48+
doc.SerializeAsV31(openApiWriter);
49+
break;
50+
case OpenApiSpecVersion.OpenApi3_0:
51+
doc.SerializeAsV3(openApiWriter);
52+
break;
53+
default:
54+
doc.SerializeAsV2(openApiWriter);
55+
break;
56+
}
57+
58+
// Serialize again with STJ after native writer serialization
59+
var finalSerialized = JsonSerializer.Serialize(doc, options);
60+
Assert.NotNull(finalSerialized); // sanity check
61+
62+
// Assert: Ensure no mutation occurred in the DOM after native serialization
63+
Assert.True(JsonNode.DeepEquals(originalSerialized, finalSerialized), "OpenAPI DOM was mutated by the native serializer.");
64+
}
65+
}
66+
67+
public class HttpMethodOperationDictionaryConverter : JsonConverter<Dictionary<HttpMethod, OpenApiOperation>>
68+
{
69+
public override Dictionary<HttpMethod, OpenApiOperation> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
70+
{
71+
throw new NotImplementedException();
72+
}
73+
74+
public override void Write(Utf8JsonWriter writer, Dictionary<HttpMethod, OpenApiOperation> value, JsonSerializerOptions options)
75+
{
76+
writer.WriteStartObject();
77+
78+
foreach (var kvp in value)
79+
{
80+
writer.WritePropertyName(kvp.Key.Method.ToLowerInvariant());
81+
JsonSerializer.Serialize(writer, kvp.Value, options);
82+
}
83+
84+
writer.WriteEndObject();
85+
}
86+
}
87+
}
88+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
openapi: '3.2.0'
2+
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
3+
info:
4+
title: Sample OpenAPI 3.2 API
5+
description: A sample API demonstrating OpenAPI 3.2 features
6+
license:
7+
name: Apache 2.0
8+
identifier: Apache-2.0
9+
version: 2.0.0
10+
summary: Sample OpenAPI 3.2 API with the latest features
11+
servers:
12+
- url: https://api.example.com/v2
13+
description: Main production server
14+
paths:
15+
/pets:
16+
get:
17+
tags:
18+
- pets
19+
summary: List all pets
20+
operationId: listPets
21+
parameters:
22+
- name: limit
23+
in: query
24+
description: How many items to return at one time (max 100)
25+
schema:
26+
exclusiveMaximum: 100
27+
exclusiveMinimum: 1
28+
type: integer
29+
responses:
30+
'200':
31+
description: A paged array of pets
32+
content:
33+
application/json:
34+
schema:
35+
$ref: https://example.com/schemas/pet.json
36+
/sample:
37+
get:
38+
summary: Sample endpoint
39+
responses:
40+
'200':
41+
description: Sample response
42+
content:
43+
application/json:
44+
schema:
45+
$id: https://example.com/schemas/person.schema.yaml
46+
$schema: https://json-schema.org/draft/2020-12/schema
47+
$comment: A schema defining a pet object with optional references to dynamic components.
48+
$vocabulary:
49+
https://json-schema.org/draft/2020-12/vocab/core: true
50+
https://json-schema.org/draft/2020-12/vocab/applicator: true
51+
https://json-schema.org/draft/2020-12/vocab/validation: true
52+
https://json-schema.org/draft/2020-12/vocab/meta-data: false
53+
https://json-schema.org/draft/2020-12/vocab/format-annotation: false
54+
$dynamicAnchor: addressDef
55+
title: Pet
56+
required:
57+
- name
58+
type: object
59+
properties:
60+
name:
61+
$comment: The pet's full name
62+
type: string
63+
address:
64+
$comment: Reference to an address definition which can change dynamically
65+
$dynamicRef: '#addressDef'
66+
description: Schema for a pet object
67+
components:
68+
schemas:
69+
Pet:
70+
$id: https://example.com/schemas/pet.json
71+
$comment: This schema represents a pet in the system.
72+
$defs:
73+
ExtraInfo:
74+
type: string
75+
required:
76+
- id
77+
- weight
78+
type: object
79+
properties:
80+
id:
81+
type: string
82+
format: uuid
83+
weight:
84+
exclusiveMinimum: 0
85+
type: number
86+
description: Weight of the pet in kilograms
87+
attributes:
88+
patternProperties:
89+
'^attr_[A-Za-z]+$':
90+
type: string
91+
type:
92+
- 'null'
93+
- object
94+
description: Dynamic attributes for the pet
95+
securitySchemes:
96+
api_key:
97+
type: apiKey
98+
name: api_key
99+
in: header
100+
security:
101+
- api_key: [ ]
102+
tags:
103+
- name: pets
104+
webhooks:
105+
newPetAlert:
106+
post:
107+
summary: Notify about a new pet being added
108+
requestBody:
109+
content:
110+
application/json:
111+
schema:
112+
type: string
113+
required: true
114+
responses:
115+
'200':
116+
description: Webhook processed successfully

0 commit comments

Comments
 (0)