Skip to content

fix: describe a container of patches, and a nullable patch, as a patch property - #237

Merged
CaffeinatedCoder merged 4 commits into
release/2.3.1from
fix/patch-container-schema-2-3-1
Oct 31, 2026
Merged

CaffeinatedCoder merged 4 commits into
release/2.3.1from
fix/patch-container-schema-2-3-1

Conversation

@CaffeinatedCoder

Copy link
Copy Markdown
Owner

Closes #232

Problem

ASP.NET leaves out items and additionalProperties for a converter-backed element, and PatchSchemaTransformer acted only on a Patch<T> itself. So in 3.1, 3.0 and 2.0 a Dictionary<string, Patch<string>> was {"type":"object"} and a List<Patch<int>> was {"type":"array"}, and a client generator typed a map of patches as a map of anything. A Patch<T> property was described properly.

Running the fix over its input class found a sibling with another cause. A Patch<T>? property was an empty PatchOfstring component ({}), and required. A List<Patch<T>?> had no items.

Change

src/CodoMetis.TypeKit.AspNetCore/PatchSchemaTransformer.cs, in two commits:

  1. Containers (Found-By: CA). A list, an array or a dictionary of patches gets the patch's own schema back where ASP.NET left it out. The schema is asked for through the context, so it's filled as a property's is: a described or replaced argument, a value object through its wrapped type, 3.0's nullable.
  2. Nullable patches (Found-By: VA). A patch is now Patch<T> or its Nullable, so a Patch<T>? is inlined, optional, and its argument's schema admitting null. It isn't described as a merge-patch field: System.Text.Json reads a null there as a null patch, the same as a property left out (measured by a request). The docs say so and point to Patch<T>. The serializer gives a Nullable<T> the element type T, and that caused two more problems, both fixed. The container check reads the kind first. ASP.NET's second visit to a nullable patch's items/additionalProperties as the patch is skipped; before, a Patch<int[]>?'s numbers admitted null.

Docs: docs/merge-patches.md and the AspNetCore README say how a container of patches and a nullable patch are described. design/patch.md's row, a CHANGELOG line under 2.3.1 "Fixed", 5 claims in test/oracles/claims.json (all held by PatchSchemaTests), and PatchSchemaTests in axes.md for the version and placement axes.

Tests

PatchSchemaTests:

  • A_patch_in_any_position_is_described_as_a_patch_property_is: the catalogue's 14 arguments (now with Lines[] and Dictionary<string, int>) in 8 positions (property, list, array, Dictionary<string,>, IReadOnlyDictionary<string,>, a nullable element, a list in a dictionary, a nullable property), plus a list as the whole body, and an MVC controller. It runs over OpenAPI 3.1, 3.0 and 2.0 × components / InlineValueObjects / inlined by the host (9 rows). A host that inlines everything has no polymorphic argument, because ASP.NET rejects a plain property of one too.
  • A_patch_in_a_container_accepts_null_and_otherwise_exactly_what_its_argument_accepts: a JSON Schema validator judges each element against the argument's own schema (3.1, 3.0).
  • A_map_of_patches_reads_a_value_and_a_null_as_the_document_says: a real request, so the document's words match what the endpoint reads.

Reverted per verify-the-guard: with the fix reverted, the 11 new document rows fail. Seeded one at a time, the dictionary arm, the nullable argument, the kind check, the nullable description and the guard's items and additionalProperties arms each fail 9 or 11. A registration of a component target that no seed caught was measured unneeded and dropped.

Locally through tools/BuildVerdict: AspNetCore 2088 passed, Conventions 2016, Samples 150.

🤖 Generated with Claude Code

CaffeinatedCoder and others added 2 commits October 31, 2026 08:36
…patch property

ASP.NET leaves out items and additionalProperties for a converter-backed
element, and PatchSchemaTransformer acted only on a Patch<T> itself. So a
Dictionary<string, Patch<string>> was {"type":"object"} and a
List<Patch<int>> {"type":"array"}, admitting anything, in 3.1, 3.0 and
2.0, while a Patch<T> property was its argument's schema admitting null.
The container branch now puts the patch's own schema back where ASP.NET
left it out, asked for through the context, so it is filled as a property
is: a described or replaced argument, a value object through its wrapped
type, 3.0's nullable form.

Input class: where a Patch<T> sits. Catalogue: PatchSchemaTests'
arguments (12 kinds, now 14 with Lines[] and Dictionary<string, int>, a
list of a component and a dictionary) in 6 positions (property, list,
array, Dictionary<string,>, IReadOnlyDictionary<string,>, a list in a
dictionary) plus a list as the whole body, in a minimal API and an MVC
controller, over OpenAPI 3.1, 3.0 and 2.0 and the three value-object
placements (components, InlineValueObjects, inlined by the host). A host
that inlines everything has no polymorphic argument: ASP.NET refuses a
plain property of one too. A wire test reads a map and a list of patches
as the document says they are read. The nullable patch the catalogue
found is the next commit.

Reverted per verify-the-guard: the 11 new document rows fail; the
dictionary arm alone fails them too.

Closes #232

Defect-Class: FCI
Found-By: CA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an empty required component

Found running the container fix over its input class: a Patch<T>?
property was an empty PatchOfstring component, admitting anything, and
required, and a List<Patch<T>?> had no items. The transformer asked only
whether a type was Patch<T> itself; it now sees through Nullable<T>, so a
nullable patch is inlined, optional, and its argument's schema admitting
null, as an element and as a property.

It is not described as a merge-patch field: System.Text.Json reads a null
into a Nullable<Patch<T>> as null, never the Null state, the same as an
absent property (measured by a request in the wire test). The docs say so
and point to Patch<T>.

Two quirks of the same cause: the serializer gives a Nullable<T> the
element type T. So the container check read a nullable patch as a
container, and now reads the kind first; and ASP.NET visits a nullable
patch's items and additionalProperties as the patch, which made a
Patch<int[]>?'s numbers admit null. Those schemas are recorded and
skipped. A registration of a reference's target no test caught was
measured unneeded (the Lines component stays clean) and dropped.

Catalogue: the previous commit's, with a nullable element and a nullable
property (8 positions). Reverted per verify-the-guard: 11 rows fail; each
of the nullable argument, the kind check, the description and the guard's
items and additionalProperties arms fails 9 or 11.

Refs #232

Defect-Class: FCI
Found-By: VA
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder CaffeinatedCoder added this to the 2.3.1 milestone Oct 31, 2026
@CaffeinatedCoder CaffeinatedCoder added bug Something isn't working silent Compiles or answers and does less than it appears to severity/low Cosmetic, message or documentation-level difference package/aspnetcore Affects CodoMetis.TypeKit.AspNetCore labels Oct 31, 2026
CaffeinatedCoder and others added 2 commits October 31, 2026 08:39
The fix of #232 turned on two facts of System.Text.Json: a Nullable<T>
contract reports T as its element type, which Microsoft.AspNetCore.OpenApi
walks items and additionalProperties with, and a JSON null read into a T?
is null without asking T's converter, even one that handles null. The
second is what the merge-patches page now tells a reader of Patch<T>?.
KB057 characterizes both against a plain T as the control.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CaffeinatedCoder
CaffeinatedCoder merged commit cc72fa4 into release/2.3.1 Oct 31, 2026
10 checks passed
@CaffeinatedCoder
CaffeinatedCoder deleted the fix/patch-container-schema-2-3-1 branch October 31, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working package/aspnetcore Affects CodoMetis.TypeKit.AspNetCore severity/low Cosmetic, message or documentation-level difference silent Compiles or answers and does less than it appears to

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant