Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.Writers;
using Xunit;

namespace Microsoft.OpenApi.Readers.Tests.V31Tests
{
public class OpenApiDocumentSerializationTests
{
private const string SampleFolderPath = "V31Tests/Samples/OpenApiDocument/";

[Fact]
public async Task Serialize_DoesNotMutateDom()
{
// Arrange
var filePath = Path.Combine(SampleFolderPath, "docWith31properties.json");
var (doc, _) = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);

// Act: Serialize using System.Text.Json
var options = new JsonSerializerOptions
{
Converters =
{
new HttpMethodOperationDictionaryConverter()
},
};
var originalSerialized = JsonSerializer.Serialize(doc, options);
Assert.NotNull(originalSerialized); // sanity check

// Serialize using native OpenAPI writer
var jsonWriter = new StringWriter();
var openApiWriter = new OpenApiJsonWriter(jsonWriter);
doc.SerializeAsV31(openApiWriter);

// Serialize again with STJ after native writer serialization
var finalSerialized = JsonSerializer.Serialize(doc, options);
Assert.NotNull(finalSerialized); // sanity check

// Assert: Ensure no mutation occurred in the DOM after native serialization
Assert.True(JsonNode.DeepEquals(originalSerialized, finalSerialized), "OpenAPI DOM was mutated by the native serializer.");
}
}

public class HttpMethodOperationDictionaryConverter : JsonConverter<Dictionary<HttpMethod, OpenApiOperation>>
{
public override Dictionary<HttpMethod, OpenApiOperation> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException();
}

public override void Write(Utf8JsonWriter writer, Dictionary<HttpMethod, OpenApiOperation> value, JsonSerializerOptions options)
{
writer.WriteStartObject();

foreach (var kvp in value)
{
writer.WritePropertyName(kvp.Key.Method.ToLowerInvariant());
JsonSerializer.Serialize(writer, kvp.Value, options);
}

writer.WriteEndObject();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
{
"openapi": "3.1.1",
"info": {
"title": "Sample OpenAPI 3.1 API",
"description": "A sample API demonstrating OpenAPI 3.1 features",
"version": "2.0.0",
"summary": "Sample OpenAPI 3.1 API with the latest features",
"license": {
"name": "Apache 2.0",
"identifier": "Apache-2.0"
}
},
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"servers": [
{
"url": "https://api.example.com/v2",
"description": "Main production server"
}
],
"webhooks": {
"newPetAlert": {
"post": {
"summary": "Notify about a new pet being added",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
},
"responses": {
"200": {
"description": "Webhook processed successfully"
}
}
}
}
},
"paths": {
"/pets": {
"get": {
"summary": "List all pets",
"operationId": "listPets",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"exclusiveMinimum": 1,
"exclusiveMaximum": 100
}
}
],
"responses": {
"200": {
"description": "A paged array of pets",
"content": {
"application/json": {
"schema": {
"$ref": "https://example.com/schemas/pet.json"
}
}
}
}
}
}
},
"/sample": {
"get": {
"summary": "Sample endpoint",
"responses": {
"200": {
"description": "Sample response",
"content": {
"application/json": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/schemas/person.schema.yaml",
"$comment": "A schema defining a pet object with optional references to dynamic components.",
"$vocabulary": {
"https://json-schema.org/draft/2020-12/vocab/core": true,
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
"https://json-schema.org/draft/2020-12/vocab/validation": true,
"https://json-schema.org/draft/2020-12/vocab/meta-data": false,
"https://json-schema.org/draft/2020-12/vocab/format-annotation": false
},
"title": "Pet",
"description": "Schema for a pet object",
"type": "object",
"properties": {
"name": {
"type": "string",
"$comment": "The pet's full name"
},
"address": {
"$dynamicRef": "#addressDef",
"$comment": "Reference to an address definition which can change dynamically"
}
},
"required": [
"name"
],
"$dynamicAnchor": "addressDef"
}
}
}
}
}
}
}
},
"components": {
"securitySchemes": {
"api_key": {
"type": "apiKey",
"name": "api_key",
"in": "header"
}
},
"schemas": {
"Pet": {
"$id": "https://example.com/schemas/pet.json",
"type": "object",
"required": [
"id",
"weight"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"weight": {
"type": "number",
"exclusiveMinimum": 0,
"description": "Weight of the pet in kilograms"
},
"attributes": {
"type": [
"object",
"null"
],
"description": "Dynamic attributes for the pet",
"patternProperties": {
"^attr_[A-Za-z]+$": {
"type": "string"
}
}
}
},
"$comment": "This schema represents a pet in the system.",
"$defs": {
"ExtraInfo": {
"type": "string"
}
}
}
}
}
}