Skip to content

Fix JsonSchemaExporter dropping null branch for nullable floating-point composition schemas#129530

Draft
Copilot wants to merge 4 commits into
mainfrom
copilot/fix-jsonschemaexporter-null-branch
Draft

Fix JsonSchemaExporter dropping null branch for nullable floating-point composition schemas#129530
Copilot wants to merge 4 commits into
mainfrom
copilot/fix-jsonschemaexporter-null-branch

Conversation

Copilot AI commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

JsonSchemaExporter omitted null-acceptance for CLR-nullable types (double?, float?) whose schema is emitted as an anyOf composition — which happens for IEEE floating-point types under JsonNumberHandling.AllowNamedFloatingPointLiterals. The same property exports correctly as {"type":["number","null"]} under default number handling, so a double? was advertised to downstream JSON-Schema/OpenAPI consumers as a non-nullable number.

Description

  • Root causeJsonSchema.MakeNullable only added JsonSchemaType.Null when the schema carried a concrete type keyword. Composition schemas (anyOf) have Type == Any, so MakeNullable was a no-op and the inherited null branch was lost.
  • Fix — Handle the null branch in the nullable-value-type path (the NullableElementConverter block in JsonSchemaExporter.MapJsonSchemaCore), mirroring the existing enum handling. This code only runs for Nullable<T>, where the element schema can only be an anyOf when it is an IEEE floating-point type formatted under AllowNamedFloatingPointLiterals; in that case JsonSchemaType.Null is folded into the numeric branch (producing { "type": ["number","null"] }). The general-purpose MakeNullable method is left unchanged so it never affects arbitrary anyOf compositions such as polymorphic object unions.
  • Tests — Added coverage for double?, float?, and a POCO with nullable floating-point properties under global AllowNamedFloatingPointLiterals, across the reflection and source-gen test contexts.

Resulting export for a nullable property:

// double? under AllowNamedFloatingPointLiterals
{"anyOf":[{"type":["number","null"]},{"enum":["NaN","Infinity","-Infinity"]}]}

Copilot AI requested review from Copilot and removed request for Copilot June 17, 2026 16:32
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 17, 2026 17:06
Copilot AI changed the title [WIP] Fix JsonSchemaExporter omitting null branch for nullable numbers Fix JsonSchemaExporter dropping null branch for nullable anyOf composition schemas Jun 17, 2026
Copilot AI requested a review from eiriktsarpalis June 17, 2026 17:07
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

@eiriktsarpalis eiriktsarpalis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This doesn't look right.

}
}

anyOf.Add(new JsonSchema { Type = JsonSchemaType.Null });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this happen even if AnyOf is the empty list? Are you sure this is desirable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Reworked MakeNullable to fold null into the first anyOf branch that carries a concrete type, rather than appending a standalone { "type": "null" } branch. An empty anyOf (or one whose branches are all type-less) now leaves the schema untouched instead of gaining a spurious null branch. Fixed in 11a4bcb.

"anyOf": [
{ "type": "number" },
{ "enum": ["NaN", "Infinity", "-Infinity"] },
{ "type": "null" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks invalid. The expected schema should have had the first subschema be { "type" : ["number","null"] }.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated — the first subschema is now { "type": ["number","null"] } and the separate null branch is gone. Fixed in 11a4bcb.

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 18, 2026 13:22

Copilot AI commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

@copilot This doesn't look right.

Agreed — the original approach was wrong. I've reworked it in 3279084a: instead of appending a standalone { "type": "null" } branch to the anyOf, MakeNullable now folds null into the first branch that carries a concrete type. So a double? under AllowNamedFloatingPointLiterals now exports as:

{"anyOf":[{"type":["number","null"]},{"enum":["NaN","Infinity","-Infinity"]}]}

This also handles the empty/type-less anyOf edge case (no spurious null branch) and skips branches that already permit null. The test expectations were corrected accordingly. (Note: my two earlier thread replies referenced an incorrect short hash; the correct commit is 3279084a.)

return;
}

if (firstTypedBranch is null && branch.Type is not JsonSchemaType.Any)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I fail to see how this fix is valid in the general case. If anyOf contained objects, this code would erroneously insert null in the first object in the list which we clearly don't want to do. What we should be fixing here is handling specifically for nullable structs when number handling is specified, not inaccurate post hoc patching like is being attempted here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I reverted the MakeNullable change entirely and moved the fix into the nullable-value-type path (NullableElementConverter block in JsonSchemaExporter), alongside the existing enum handling. This code only runs for Nullable<T>, where the element schema can only be an anyOf when it's an IEEE floating-point type under AllowNamedFloatingPointLiterals — so the null is folded into the numeric branch ({ "type": ["number","null"] }) and never touches object/polymorphic compositions. Fixed in c81c58a.

…, revert MakeNullable

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot June 18, 2026 16:01
Copilot AI changed the title Fix JsonSchemaExporter dropping null branch for nullable anyOf composition schemas Fix JsonSchemaExporter dropping null branch for nullable floating-point composition schemas Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants