Skip to content
Draft
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
21 changes: 1 addition & 20 deletions src/Microsoft.OpenApi/Models/OpenApiRequestBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,26 +130,7 @@ public IEnumerable<IOpenApiParameter> ConvertToFormDataParameters(IOpenApiWriter
{
foreach (var property in properties)
{
var paramSchema = property.Value.CreateShallowCopy();
if ((paramSchema.Type & JsonSchemaType.String) == JsonSchemaType.String
&& ("binary".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)
|| "base64".Equals(paramSchema.Format, StringComparison.OrdinalIgnoreCase)))
{
var updatedSchema = paramSchema switch
{
OpenApiSchema s => s, // we already have a copy
// we have a copy of a reference but don't want to mutate the source schema
// TODO might need recursive resolution of references here
OpenApiSchemaReference r when r.Target is not null => (OpenApiSchema)r.Target.CreateShallowCopy(),
OpenApiSchemaReference => throw new InvalidOperationException("Unresolved reference target"),
_ => throw new InvalidOperationException("Unexpected schema type")
};

updatedSchema.Type = "file".ToJsonSchemaType();
updatedSchema.Format = null;
paramSchema = updatedSchema;

}
var paramSchema = property.Value;
yield return new OpenApiFormDataParameter()
{
Description = paramSchema.Description,
Expand Down
48 changes: 41 additions & 7 deletions src/Microsoft.OpenApi/Models/OpenApiSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,13 @@ private void SerializeInternal(IOpenApiWriter writer, OpenApiSpecVersion version
writer.WriteProperty(OpenApiConstants.Description, Description);

// format
writer.WriteProperty(OpenApiConstants.Format, Format);
var format = Format;
if (version < OpenApiSpecVersion.OpenApi3_1)
{
format ??= GetKnownTypeAndFormatPreOpenApi31()?.Format;
}

writer.WriteProperty(OpenApiConstants.Format, format);

// default
writer.WriteOptionalObject(OpenApiConstants.Default, Default, (w, d) => w.WriteAny(d));
Expand Down Expand Up @@ -756,7 +762,8 @@ private void WriteV3CompatibilityKeywords(IOpenApiWriter writer, Action<IOpenApi
internal void WriteAsItemsProperties(IOpenApiWriter writer)
{
// type
writer.WriteProperty(OpenApiConstants.Type, (Type & ~JsonSchemaType.Null)?.ToFirstIdentifier());
var typeToUse = Type ?? GetKnownTypeAndFormatPreOpenApi31()?.Type;
writer.WriteProperty(OpenApiConstants.Type, (typeToUse & ~JsonSchemaType.Null)?.ToFirstIdentifier());

// format
WriteFormatProperty(writer);
Expand Down Expand Up @@ -813,7 +820,8 @@ private void WriteFormatProperty(IOpenApiWriter writer)
var formatToWrite = Format;
if (string.IsNullOrEmpty(formatToWrite))
{
formatToWrite = AllOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ??
formatToWrite = GetKnownTypeAndFormatPreOpenApi31()?.Format ??
AllOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ??
AnyOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format ??
OneOf?.FirstOrDefault(static x => !string.IsNullOrEmpty(x.Format))?.Format;
}
Expand Down Expand Up @@ -1008,7 +1016,11 @@ private void SerializeAsV2(

private void SerializeTypePropertyForVersion2(IOpenApiWriter writer)
{
if (Type is not { } type || type == JsonSchemaType.Null)
// TODO: Handle "file" type for 2.0.
// Spec https://spec.openapis.org/oas/v2.0.html#data-types
var typeToUse = Type ?? GetKnownTypeAndFormatPreOpenApi31()?.Type;

if (typeToUse is not { } type || type == JsonSchemaType.Null)
{
return;
}
Expand All @@ -1027,7 +1039,13 @@ private void SerializeTypePropertyForVersion2(IOpenApiWriter writer)
/// </summary>
private void SerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, OpenApiSpecVersion version, Action<IOpenApiWriter, IOpenApiSerializable> callback)
{
if (Type is not { } type)
var type = Type;
if (version < OpenApiSpecVersion.OpenApi3_1)
{
type ??= GetKnownTypeAndFormatPreOpenApi31()?.Type;
}

if (type is null)
{
return;
}
Expand All @@ -1042,7 +1060,7 @@ private void SerializeTypePropertyForVersion3AndLater(IOpenApiWriter writer, Ope
var typeWithoutNull = type & ~JsonSchemaType.Null;
var hasNull = typeWithoutNull != type;
var arrayWithoutNull = (from JsonSchemaType flag in jsonSchemaTypeValues
where typeWithoutNull.HasFlag(flag)
where typeWithoutNull.Value.HasFlag(flag)
select flag).ToArray();

// - If we have more than one type (excluding null), we have to use anyOf/oneOf.
Expand Down Expand Up @@ -1073,7 +1091,7 @@ where typeWithoutNull.HasFlag(flag)
else
{
var array = (from JsonSchemaType flag in jsonSchemaTypeValues
where type.HasFlag(flag)
where type.Value.HasFlag(flag)
select flag).ToArray();

if (array.Length > 1)
Expand Down Expand Up @@ -1157,6 +1175,22 @@ private void SerializeNullable(IOpenApiWriter writer, OpenApiSpecVersion version
}
}

private (JsonSchemaType Type, string Format)? GetKnownTypeAndFormatPreOpenApi31()
{
// https://spec.openapis.org/oas/v3.2.0.html#migrating-binary-descriptions-from-oas-3-0
if (Type is JsonSchemaType.String or (JsonSchemaType.String | JsonSchemaType.Null) && ContentEncoding == "base64")
{
return (Type.Value, "byte");
}

if (Type is null && ContentEncoding is null && !string.IsNullOrEmpty(ContentMediaType))
{
return (JsonSchemaType.String, "binary");
}

return null;
}

#if NET5_0_OR_GREATER
private static readonly Array jsonSchemaTypeValues = System.Enum.GetValues<JsonSchemaType>();
#else
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

using System.Text.Json.Nodes;
Expand Down Expand Up @@ -74,6 +74,16 @@ internal static partial class OpenApiV2Deserializer
{
var schema = GetOrCreateSchema(o);
schema.Type = type.ToJsonSchemaType();
// TODO: This should be represented using the 3.2 approach.
// The object model must reflect the "latest" version of the spec.
// Note that for parameters in 2.0, the "file" type is specified directly
// on the parameter object. But for responses, the "file" type is an
// extension of the Json Schema object, as in, it's not allowed by
// Json Schema Draft 4, but is allowed as an OpenAPI 2.0 extension.
// All that should be handled correctly.
// The deserialization logic should try to map everything to the "3.2" way
// of doing things.
// And serialization should assume that the object model is in the "3.2" way of doing things.
if ("file".Equals(type, StringComparison.OrdinalIgnoreCase))
{
schema.Format = "binary";
Expand Down
32 changes: 32 additions & 0 deletions src/Microsoft.OpenApi/Reader/V2/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,18 @@ internal static partial class OpenApiV2Deserializer
OpenApiConstants.PatternPropertiesExtension,
(o, n, t, c) => o.PatternProperties = n.CreateMap(LoadSchema, t, c)
},
{
OpenApiConstants.ContentEncodingExtension,
(o, n, _, _) => o.ContentEncoding = n.GetScalarValue()
},
{
OpenApiConstants.ContentMediaTypeExtension,
(o, n, _, _) => o.ContentMediaType = n.GetScalarValue()
},
{
OpenApiConstants.ContentSchemaExtension,
(o, n, doc, c) => o.ContentSchema = LoadSchema(n, doc, c)
},
};

private static readonly PatternFieldMap<OpenApiSchema> _openApiSchemaPatternFields = new PatternFieldMap<OpenApiSchema>
Expand Down Expand Up @@ -308,6 +320,26 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
}
}

// The object model represents the latest version of the spec.
// https://spec.openapis.org/oas/v3.2.0.html#migrating-binary-descriptions-from-oas-3-0
// When we deserialize from V2, we detect the "old way" of specifying binary descriptions, and
// transform it in the object model to the latest thing.
Comment thread
Youssef1313 marked this conversation as resolved.
if (schema.Type.HasValue && schema.Type.Value.HasFlag(JsonSchemaType.String) &&
schema.Format == "byte" &&
schema.ContentEncoding is null or "base64")
{
schema.ContentEncoding = "base64";
schema.Format = null;
}

if (schema.Type.HasValue && schema.Type.Value == JsonSchemaType.String &&
schema.Format == "binary")
{
schema.ContentMediaType ??= "application/octet-stream";
schema.Format = null;
schema.Type = null;
}
Comment on lines +338 to +341

return schema;
}
}
Expand Down
20 changes: 20 additions & 0 deletions src/Microsoft.OpenApi/Reader/V3/OpenApiSchemaDeserializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,26 @@ public static IOpenApiSchema LoadSchema(JsonNode node, OpenApiDocument hostDocum
}
}

// The object model represents the latest version of the spec.
// https://spec.openapis.org/oas/v3.2.0.html#migrating-binary-descriptions-from-oas-3-0
// When we deserialize from V3, we detect the "old way" of specifying binary descriptions, and
// transform it in the object model to the latest thing.
if (schema.Type.HasValue && schema.Type.Value.HasFlag(JsonSchemaType.String) &&
schema.Format == "byte" &&
schema.ContentEncoding is null or "base64")
{
schema.ContentEncoding = "base64";
schema.Format = null;
}

if (schema.Type.HasValue && schema.Type.Value == JsonSchemaType.String &&
schema.Format == "binary")
{
schema.ContentMediaType ??= "application/octet-stream";
schema.Format = null;
schema.Type = null;
}
Comment on lines +456 to +459

return schema;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,75 @@ public async Task SerializeSchemaWithOnlyNullableShouldSucceed()

Assert.Equal(expected.MakeLineBreaksEnvironmentNeutral(), schemaString.MakeLineBreaksEnvironmentNeutral());
}

private static OpenApiSchema LoadV2Schema(string json)
=> Assert.IsType<OpenApiSchema>(
OpenApiV2Deserializer.LoadSchema(JsonNode.Parse(json), new(), new ParsingContext(new())));

private static string SerializeAsV2(OpenApiSchema schema)
{
var writer = new StringWriter();
schema.SerializeAsV2(new OpenApiJsonWriter(writer));
return writer.ToString();
}

// The object model represents the latest version of the spec, so v2 binary descriptions
// are normalized on read and reconstructed on write.
// https://spec.openapis.org/oas/v3.2.0.html#migrating-binary-descriptions-from-oas-3-0
[Fact]
public void ParseSchemaWithByteFormatNormalizesToContentEncoding()
{
var schema = LoadV2Schema("""{ "type": "string", "format": "byte" }""");

Assert.Equal(JsonSchemaType.String, schema.Type);
Assert.Equal("base64", schema.ContentEncoding);
Assert.Null(schema.Format);
}

[Fact]
public void ParseSchemaWithBinaryFormatNormalizesToContentMediaType()
{
var schema = LoadV2Schema("""{ "type": "string", "format": "binary" }""");

Assert.Null(schema.Type);
Assert.Equal("application/octet-stream", schema.ContentMediaType);
Assert.Null(schema.Format);
}

[Fact]
public void ParseSchemaWithContentEncodingExtensionAssignsContentProperties()
{
var schema = LoadV2Schema("""
{
"type": "string",
"x-jsonschema-contentEncoding": "base64",
"x-jsonschema-contentMediaType": "image/png",
"x-jsonschema-contentSchema": { "type": "array" }
}
""");

Assert.Equal("base64", schema.ContentEncoding);
Assert.Equal("image/png", schema.ContentMediaType);
Assert.Equal(JsonSchemaType.Array, schema.ContentSchema?.Type);
Assert.Empty(schema.Extensions ?? new Dictionary<string, IOpenApiExtension>());
}

[Theory]
[InlineData("""{ "type": "string", "format": "byte" }""")]
[InlineData("""{ "type": "string", "format": "binary" }""")]
public void SchemaWithBinaryDescriptionRoundTripsThroughV2(string original)
{
var schema = LoadV2Schema(original);

var serialized = SerializeAsV2(schema);
Assert.True(JsonNode.DeepEquals(JsonNode.Parse(original), JsonNode.Parse(serialized)));

// Reading our own output must produce an equivalent model.
var reparsed = LoadV2Schema(serialized);
Assert.Equal(schema.Type, reparsed.Type);
Assert.Equal(schema.Format, reparsed.Format);
Assert.Equal(schema.ContentEncoding, reparsed.ContentEncoding);
Assert.Equal(schema.ContentMediaType, reparsed.ContentMediaType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,28 @@ public void DefaultEmptyCollectionShouldRoundTrip()
Assert.Empty(resultingArray);
}

// The pre-3.1 binary description is only normalized when reading v2/v3.0 documents.
// From 3.1 onwards "format" is a plain annotation and must be preserved verbatim.
[Theory]
[InlineData("binary")]
[InlineData("byte")]
public void BinaryFormatIsNotNormalizedInV31(string format)
{
var serializedSchema = $$"""
{
"type": "string",
"format": "{{format}}"
}
""";

var schema = OpenApiModelFactory.Parse<OpenApiSchema>(serializedSchema, OpenApiSpecVersion.OpenApi3_1, new(), out _, "json", SettingsFixture.ReaderSettings);

Assert.Equal(JsonSchemaType.String, schema.Type);
Assert.Equal(format, schema.Format);
Assert.Null(schema.ContentEncoding);
Assert.Null(schema.ContentMediaType);
}

[Fact]
public void DefaultNullIsLossyDuringRoundTripJson()
{
Expand Down
Loading