Repository navigation
fix: a nullable value object that is a component keeps its null as a get-only property, a field and an oblivious one - #63
Merged
Conversation
…get-only property, a field and an oblivious one
C21OpenapiSchemaParityProbe found, and the code-analyst confirmed on 2026-10-18 by
reproduction and a seeded fix, that a nullable value object written as a component lost
its null in three positions where its wrapped twin kept it: a get-only property bound by
the constructor, a field under IncludeFields, and a reference-type property in
#nullable disable code. The value object's property was the bare $ref; the twin's
admitted null. Decision 36 and the AspNetCore README promise null wherever a nullable
value object appears.
Cause. ASP.NET adds oneOf [null, $ref] around a component only when the schema carries
x-is-nullable-property, set by ShouldApplyNullablePropertySchema, which needs a
PropertyInfo whose NullabilityInfoContext WriteState is Nullable: not a get-only
property (Unknown), a field (a FieldInfo) or an oblivious reference type. The inlined
wrapped type takes its null from JsonPropertyInfo.IsGetNullable || IsSetNullable, true
in all three. ValueObjectSchemaTransformer left the null to ASP.NET wherever the value
object is a component.
Change. Where the value object is not inlined, has a component and the property's
contract says it is nullable, the transformer marks the schema with
x-is-nullable-property, so ASP.NET writes the null-or as it does for a settable
annotated property. That reads ASP.NET's internal key (OpenApiConstants.NullableProperty,
Microsoft.AspNetCore.OpenApi 10.0.12) the way decision 106 reads x-schema-id. It never
takes a mark away. The analyst's candidate also excluded a value object over object;
that is unreachable, since the generators refuse IValue<object> (CMTK1005), so it is
left out. By the maintainer's decision the rule holds under every OpenAPI version: under
2.0 the property becomes ASP.NET's documented form, allOf [{x-nullable: true}] without
the reference (KB-026, the loss the README states for a nullable value-object property),
as a settable annotated one already was.
Tests. ComponentNullablePropertyTests: the three positions under 3.1, 3.0 and 2.0, each
against the settable annotated property of the same value object beside it (ASP.NET's
own form) and against the twin (TimeSpan? for ProbeDuration?, string for ProbeLabel):
9 cases. Reverted, all 9 fail with the bare $ref where the null-or (3.x) or the 2.0
null flag was expected. PolymorphicBaseTests' fixture gains a get-only nullable value
object, which the base's own branch now writes as the derived branch does (it passes
either way; coverage, not a guard).
Variants (decision 95). The input class: a property's nullability as
NullabilityInfoContext reads it (ASP.NET's, for a component) against JsonPropertyInfo
(the serializer's, and ASP.NET's for an inlined schema). There is no catalogue for it, so
the test holds one: 31 members, each a value object's beside its wrapped type's, under
3.1, 3.0 and 2.0, with value objects as components and inlined, and RespectNullableAnnotations
off and on (372 cases). Struct value object: settable, get-only, computed, field,
readonly field, init, required init, positional record, a [JsonInclude] private setter,
a get-only property of a struct, and non-nullable settable, get-only and field. Class
value object: annotated settable, get-only, field, required init and positional; oblivious
settable, get-only and field; [AllowNull], [MaybeNull], [DisallowNull], [NotNull]; typed
by a type parameter as settable, get-only, field and required init; non-nullable settable
and get-only. The type-parameter members and the RespectNullableAnnotations axis follow
the catalogue of a500025, an unpushed fix of the same defect made in another session.
Reverted, 93 of the class's 381 cases fail: the 9 above and 14 members in every component
row. Besides the three named, they are siblings of the same cause, fixed by the same mark:
a computed property, a readonly field, a get-only property of a struct, [MaybeNull],
[DisallowNull], and a type parameter's get-only property and field. Already right, and
unchanged: settable, init, required init, positional, the private setter, [AllowNull],
[NotNull], a type parameter's settable and required-init property, every non-nullable
member, and every inlined row. The shape.* catalogue has no value for a get-only property,
an oblivious one or a nullability attribute; they are held in the test, not added to
.claude/probe-axes.md, since the axes file is in every probe's identity (decision 108).
AspNetCore README and docs/aspnet-core.md: no sentence changes; the README already says a
nullable property is the component or null, and loses its type under 2.0.
Defect class SDL, silent does-less: the document builds and every value object is described, and
the null is simply missing, with no error and no setting involved. Two of the three positions need
no option at all (a get-only property, an oblivious one), so the cause is not a framework
configuration (FCI), and no more specific class of decision 94's table fits.
Defect-Class: SDL
Found-By: PR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow's axes
With the fix before this commit, a nullable value object that is a component is
OpenAPI 2.0's allOf [{x-nullable: true}] without the reference wherever the serializer's
contract says the property is nullable. HttpBaseline.SchemaParity cited the not-promised
entry openapi-2-nullable-component-loses-its-type only where side (1), ASP.NET alone,
writes the value object's position the same, and ASP.NET alone adds that null only for
a settable annotated property. Rows 178 (a record class over DateTime, generic container)
and 194 (a record class over TimeSpan, nullable-property in a record class) then failed:
the twin's property keeps its type beside x-nullable, the value object's has the null
flag alone.
The check now cites the same entry by the row's axes as well: OpenAPI 2.0, a nullable
value object, not inlined (CreateSchemaReferenceId other than null-inlines-all, the
entry's own axes), where the value object's property is the null flag without its
reference or type and the twin's property admits null.
prober try --probe C21OpenapiSchemaParityProbe, with the rows file of release/2.2.0:
release/2.2.0 (938d8f8): expected-refusal 129, inapplicable 4, inconclusive 6, pass 155.
With the fix alone: violation 2 (rows 178, 194), pass 154, inapplicable 3. With this
commit: expected-refusal 129, inapplicable 5, inconclusive 6, pass 154, exit 0; row 181
(a validated class over Uri) went from inapplicable to pass, and 178 and 194 are the
cited loss.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix's new lines in ValueObjectSchemaTransformer moved the context of the seed for nullable-admits-null-as-the-wrapped-does, and `prober seeds --probe C21OpenapiSchemaParityProbe` reported it as not applying (patch failed at line 98). The patch is made again against the new code, with the same defect: a nullable value object's position is Property wherever it is Nullable<T> or its property is nullable, and None as a parameter, which puts the null where ASP.NET's exporter does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CaffeinatedCoder
added a commit
that referenced
this pull request
Oct 18, 2026
F4's fix (#63), which C21's rows 38, 170, 184, 330 and 415 need. # Conflicts: # tools/probe/corpus/harness/HttpBaseline.cs
This was referenced Oct 18, 2026
Merged
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
C21OpenapiSchemaParityProbe found, and the code-analyst confirmed on 2026-10-18 by reproduction and a seeded fix, that a nullable value object written as a component lost its null in three positions where its wrapped twin kept it:
IncludeFields,#nullable disablecode.ASP.NET adds
oneOf [null, $ref]around a component only when the schema carriesx-is-nullable-property.ShouldApplyNullablePropertySchemasets it, and only for aPropertyInfowhoseNullabilityInfoContextWriteStateisNullable. That is false for a get-only property (Unknown), a field (aFieldInfo) and an oblivious reference type. The inlined twin gets its null fromJsonPropertyInfo.IsGetNullable || IsSetNullable, which is true in all three. Decision 36 and the AspNetCore README promise that a nullable value object admits null wherever it appears.Change
ValueObjectSchemaTransformer: where the value object is not inlined, has a component, and itsJsonPropertyInfois nullable, the schema gets thex-is-nullable-propertymark. ASP.NET then writes the same null-or it writes for a settable annotated property.OpenApiConstants.NullableProperty, 10.0.12), the way decision 106 readsx-schema-id.object" condition is left out. It can never apply:IValue<object>is refused at build time (CMTK1005).allOf [{x-nullable: true}]without the reference (KB-026). That is the loss the README already states, and the form a settable annotated property already had.HttpBaseline.SchemaParity, a separatetest:commit): the not-promised entryopenapi-2-nullable-component-loses-its-typeis now also cited by the row's axes. That covers OpenAPI 2.0 rows with a nullable value object that is not inlined, where its property is the null flag without its reference and the twin's admits null. The existing citation needs side (1) to show the same form, which ASP.NET alone writes only for a settable annotated property.nullable-admits-null-as-the-wrapped-doesis regenerated against the new code, with the same defect (a separatetest:commit, 89b310a).docs/aspnet-core.mdchanges: they already say a nullable property is the component or null, and that under 2.0 it loses its type.Measurement: the three positions, before and after (components, minimal API body)
ProbeDuration? P { get; }(ctor-bound) vsTimeSpan?{"type":["null","string"],"pattern":…}{"$ref":"ProbeDuration"}{"oneOf":[{"type":"null"},{"$ref":"ProbeDuration"}]}{"$ref":"ProbeDuration"}{"oneOf":[{"enum":[null],"nullable":true},{"$ref":"ProbeDuration"}]}{"$ref":"ProbeDuration"}{"allOf":[{"x-nullable":true}]}ComponentField<ProbeDuration?>.P(IncludeFields) vsTimeSpan? P;{"type":["null","string"],"pattern":…}{"$ref":"ProbeDuration"}{"oneOf":[{"type":"null"},{"$ref":"ProbeDuration"}]}{"$ref":"ProbeDuration"}{"oneOf":[{"enum":[null],"nullable":true},{"$ref":"ProbeDuration"}]}{"$ref":"ProbeDuration"}{"allOf":[{"x-nullable":true}]}#nullable disableProbeLabel Pvsstring P{"type":["null","string"]}{"$ref":"ProbeLabel"}{"oneOf":[{"type":"null"},{"$ref":"ProbeLabel"}]}{"$ref":"ProbeLabel"}{"oneOf":[{"enum":[null],"nullable":true},{"$ref":"ProbeLabel"}]}{"$ref":"ProbeLabel"}{"allOf":[{"x-nullable":true}]}Each "after" equals what ASP.NET writes for a settable annotated
ProbeDuration? Q { get; set; }/ProbeLabel? Q { get; set; }beside it in the same class. Under 2.0 the twin is{"type":"string",…,"x-nullable":true}.Tests
ComponentNullablePropertyTestschecks the three positions under 3.1, 3.0 and 2.0 (9 cases). Each is compared with the settable annotated sibling (ASP.NET's own form) and with the twin (TimeSpan?,string).$refwhere the null-or, or the 2.0 null flag, was expected. With the fix restored, all pass.NullabilityInfoContextreads it, againstJsonPropertyInfo. There is no catalogue for it, so the test holds one: 31 members × {3.1, 3.0, 2.0} × {components, inlined} ×RespectNullableAnnotations{off, on} = 372 cases.[JsonInclude]private setter, get-only on a struct, and non-nullable settable, get-only and field.[AllowNull],[MaybeNull],[DisallowNull],[NotNull]; typed by a type parameter as settable, get-only, field and required init; non-nullable settable and get-only.[MaybeNull],[DisallowNull], and a type parameter's get-only property and field.[AllowNull],[NotNull], a type parameter's settable and required-init property, every non-nullable member, and every inlined row.Found-By: VAcommit.shape.*catalogue has no value for a get-only property, an oblivious one or a nullability attribute. They are held in the test rather than added to.claude/probe-axes.md, because the axes file is part of every probe's identity (decision 108).RespectNullableAnnotationsaxis come from the catalogue of a500025, an unpushed fix of the same defect made in another session.PolymorphicBaseTestsgains a get-only nullable value object. The base's own branch (walked by TypeKit) now writes the null-or the derived branch does. This test passes either way, so it is coverage, not a guard.NullablePositionTestsin full, plus the inlined variants).$ref.CommitTrailerTests. Samples 144 passed.Probes
prober try --probe C21OpenapiSchemaParityProbe, with release/2.2.0's rows file (not #62's):DateTime, generic container) and 194 (a record class overTimeSpan, nullable property) are OpenAPI 2.0 rows. There the value object's property is now the documented null flag without its reference, while the twin's keeps its type besidex-nullable. Both now citeopenapi-2-nullable-component-loses-its-typeby the row's axes.Uri) went from inapplicable to pass.prober replay --family httpfrom an emptyPROBE_STATE_DIR, with footprints: 13 passed, 0 failed, 478 s. Every probe's row outcomes match decision 108's at release/2.2.0, except C21's above.prober seeds(decision 107): run per probe with--probefor the 13 http probes, from the replay's state. 11 of them have the transformer in their footprint, and the harness file is in all of their identities. By footprint alone, with no earlier seed run on record for this branch, it would have run all 45 probes (169 seeds). 54 of 54 seeds caught:nullable-admits-null-as-the-wrapped-does: 7 of 294 rows)The first run, at ed94ae6, found that C21's
nullable-admits-null-as-the-wrapped-doesseed no longer applied, because the fix moved its context. CI'sSeedsTestsfailed on the same thing. 89b310a regenerates the patch with the same defect, and Prober.Tests passes locally (268).Relation to other work
fix/nullable-reference-property, is a fix of the same defect made in another session. It stays unpushed; this PR was compared against it, and its source change is the same. Its type-parameter members andRespectNullableAnnotationsaxis are part of the variant catalogue here.build/gate-closes) has its own structural KB-026 branch at the same spot inHttpBaseline.SchemaParity, and it covers rows 178 and 194 too. When gate-closes takes release/2.2.0, resolve that conflict by keeping gate-closes's branch and dropping this one, together withTheNullFlagWithoutItsReference.🤖 Generated with Claude Code