Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ section is the GitHub release's notes.
- OpenAPI: the default of a parameter of a type a converter describes, or of a value object over one, such as `[DefaultValue(typeof(decimal), "1.5")]` under `DecimalStringJsonConverter`, is written as ASP.NET writes it without the converter, `1.5`, not `"1.5"` ([Parameters aren't JSON](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a non-abstract `[JsonPolymorphic]` base's own branch (`{Base}Base`) describes its value objects, patches, closed sets and described types as the derived types' branches do, where it had `{}` and a list without `items`, in OpenAPI 3.1, 3.0 and 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a list or dictionary of a nullable value object whose converter a described one replaces, such as `List<Amount?>` under a converter that writes cents, admits `null` in its elements where value objects are components ([A described type](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a nullable value-object property where value objects are components is the component or `null` wherever the underlying type's property admits `null`: get-only, a field, typed by a type parameter, under `[MaybeNull]` or `[DisallowNull]`, or without nullable annotations, where it was the bare reference; under OpenAPI 2.0 it's ASP.NET's `x-nullable` without the reference there too, as a settable one already was ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a nullable value object is its wrapped type at the same nullability in every position: an MVC `[FromBody] Quantity?` admits `null` inside its schema as `int?` does, and, where value objects are inlined, a query, header or form parameter no longer admits `null`, nor does a body, response or element of a nullable struct over `string` or `Uri` ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a nullable value object over an enum written by name lists `null` among its values as a route, query, header or form parameter, as the enum's own `DayOfWeek?` does; where value objects alone are inlined, a nullable one over an enum no longer puts a second null-or inside a body's or a response's, and as an element or a dictionary value keeps its type in OpenAPI 2.0 ([What the document says](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-the-document-says)).
- OpenAPI: a problem response an endpoint declares as `application/json`, such as `Produces<ProblemDetails>(404)` or `[ProducesResponseType<ProblemDetails>(404)]`, describes `fault` and `rule` and names its problem types, and keeps its media type; so does one of a type derived from `ProblemDetails`, such as `ProducesValidationProblem(404)` ([A Result as a response](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#a-result-as-a-response)).
Expand Down
26 changes: 23 additions & 3 deletions src/CodoMetis.TypeKit.AspNetCore/ValueObjectSchemaTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerC
// Written by a converter the options put in charge, not the generated one: not the wrapped type's.
if (DescribedSchemaTransformer.Replaced(info.Type, info.Options)) return;

// Where ASP.NET makes the value object a component, it adds the null around the reference itself.
// Where ASP.NET makes the value object a component, it adds the null around the reference itself (marked below where
// it would not).
// Inlined, the schema is this position's own, and the null goes in it where ASP.NET's exporter puts
// it into the wrapped type's schema in the same position: for a nullable property of that type, and
// elsewhere for the Nullable<T> of a wrapped value type, a parameter outside the body included. Not at
Expand All @@ -97,8 +98,17 @@ public async Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerC
DescribedSchemaTransformer.ParameterDefault(schema, info.Type, info.Options);
}

var position = !inline && SchemaReferenceId(info, context) is not null ? NullablePosition.None
: context.JsonPropertyInfo is { IsGetNullable: true } or { IsSetNullable: true } ? NullablePosition.Property
var component = !inline && SchemaReferenceId(info, context) is not null;
var nullableProperty = context.JsonPropertyInfo is { IsGetNullable: true } or { IsSetNullable: true };

// ASP.NET puts its null around the reference only for a property it reads as nullable through NullabilityInfoContext,
// a settable one annotated: not for a get-only property, a field or an oblivious reference type, whose JSON admits null
// as the serializer's contract says, and whose wrapped type's property keeps its null. So the property is marked as ASP.NET
// marks the others, under its own key (OpenApiConstants.NullableProperty, internal, as decision 106 reads x-schema-id).
if (component && nullableProperty) MarkNullableProperty(schema);

var position = component ? NullablePosition.None
: nullableProperty ? NullablePosition.Property
: Nullable.GetUnderlyingType(info.Type) is null || !wrapped.IsValueType ? NullablePosition.None
: IsParameterOutsideTheBody(context) ? NullablePosition.ParameterOutsideTheBody
: NullablePosition.Root;
Expand Down Expand Up @@ -273,6 +283,16 @@ private static void Inline(OpenApiSchema schema)
if (metadata.TryGetValue("x-ref-default", out var value) && metadata.Remove("x-ref-default") && value is JsonNode defaultValue) schema.Default ??= defaultValue;
}

/// <summary>
/// Has ASP.NET write the property as null or the reference to the value object's component, as it writes a property it
/// reads as nullable itself. It reads the mark from the schema's metadata, under a key of its own
/// (<c>OpenApiConstants.NullableProperty</c>, internal, set by <c>ApplyNullabilityContextInfo</c> and read by
/// <c>ResolveReferenceForSchema</c>; measured with Microsoft.AspNetCore.OpenApi 10.0.12). OpenAPI 2.0 writes it as its
/// <c>allOf: [{x-nullable: true}]</c>, without the reference, as for every nullable property of a component (KB-026).
/// </summary>
private static void MarkNullableProperty(OpenApiSchema schema) =>
(schema.Metadata ??= new Dictionary<string, object>())["x-is-nullable-property"] = true;

/// <summary>
/// Whether a schema of <paramref name="type"/> visited with <paramref name="parameter"/> is the parameter's own,
/// rather than an element's below it (<c>decimal</c> under <c>decimal[]</c>) or the type a value object wraps:
Expand Down
Loading
Loading