You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Preserve nulls required by structured output schemas - #1929
When structured tool output has an output schema and serializer options ignore null values, required nullable properties are omitted from structuredContent. Preserve explicit nulls for this serialization path so the result matches its schema while leaving ordinary text serialization unchanged.
Add regression coverage for record and class outputs with null and non-null values across the current protocol and 2025-11-25.
Validation passed: dotnet test in Debug and Release across the repository test suites; dotnet format whitespace --verify-no-changes; Windows x64 NativeAOT publish and execution; Release NuGet pack; and DocFX with warnings treated as errors. The Core suites passed 2,398 tests per net8.0/net9.0/net10.0 target (4 skipped) and 2,081 tests on net472 (294 skipped); ASP.NET Core passed 635 tests per modern target (30 skipped); Analyzer passed 57 tests.
Adding a data point on why this matters beyond the TypeScript 1.x client in #1915: it breaks interop with the other official SDKs' current clients, too. I hit it in a companion sample for my book. A C# server returns a positional record with nullable members (OperationStatus(string OperationId, string Kind, string Status, string? Result, string? Guidance), source), and both other SDKs' clients reject every call where one of those members is null. The repro calls the tool over stdio with an unknown ID, so Result is null:
Structured content does not match the tool's output schema: data must have required property 'result'
OK, "result": null
Both clients validate structuredContent against outputSchema by default (the spec's SHOULD), while the C# client doesn't, so a C# server passes its own end-to-end tests and only fails when a Python or TypeScript client calls it. The equivalent Python (Pydantic str | None) and TypeScript (zod .nullable()) servers already put an explicit null on the wire, so with this change the three SDKs agree.
Until this ships, the per-member workaround that worked for me is [property: JsonIgnore(Condition = JsonIgnoreCondition.Never)] on the nullable record parameters.
The reason will be displayed to describe this comment to others. Learn more.
This fixes a real interoperability issue: required nullable properties must be present in structured output, and Python and TypeScript clients reject results when those properties are omitted.
I found one narrow compatibility regression with explicit output schemas, noted inline. Two related ignore-policy gaps also remain, but those predate this PR.
The reason will be displayed to describe this comment to others. Learn more.
Three remaining issues reproduced against 0170ae6: caller-filter bypass for default-ignored values, incompatible intermediate union repairs, and missing dependentRequired handling.
The ListResourceTemplatesAsync_WithRequestParams_ReturnsRawResult(clientId: "everything") failure is reproducible on the clean baseline and is not introduced by this PR.
Command: dotnet test tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj --framework net10.0 --filter FullyQualifiedName~ListResourceTemplatesAsync_WithRequestParams_ReturnsRawResult
Baseline result: test_server passed; everything failed during initialization with System.TimeoutException: Initialization timed out while launching @modelcontextprotocol/server-everything@2026.1.26 through npx.
The same everything initialization timeout occurred on the PR head.
The focused structured-output tests pass independently; this integration failure is external to the structured-content changes in this PR.
ProtocolTool.OutputSchema is publicly mutable, but this cached schema never changes after the first invocation. For example, invoke once with a schema that permits an omitted note, then replace OutputSchema with one requiring nullable note. Subsequent calls validate against the old permissive schema and return before restoring the null, even though tools/list publishes the new requirement. Cache the compiled schema together with its source JsonElement and refresh it when that value changes, so traversal and validation always use the same schema.
[!NOTE]
AI-generated by GitHub Copilot.
Traverse array contains schemas for recovery candidate discovery
Array traversal ignores contains. For example, an explicit schema {"type":"array","contains":{"type":"object","properties":{"note":{"type":["string","null"]}},"required":["note"]}} rejects an initially serialized [{"id":"42"}]. Even when the complete snapshot contains note:null, no candidate is collected because there is no items or prefixItems schema, so the required null remains missing. Traverse contains for candidate discovery too, retaining final schema validation to enforce its cardinality constraints, and add a regression for this case.
[!NOTE]
AI-generated by GitHub Copilot.
Preserve converter-produced JSON nulls during required value recovery
A null JsonNode can represent successful serialization to JSON null, not just failure. For a required RequestId property with member-level WhenWritingDefault, default(RequestId) is omitted from both snapshots, but its converter correctly writes JSON null (Protocol/RequestId.cs:89-95). Recovery then rejects that value here because the boxed struct is non-null and the serialized node is null, leaving the required property absent. Return serialization success separately from the nullable node so converter-produced nulls can be restored without treating serialization failures as success.
[!NOTE]
AI-generated by GitHub Copilot.
Resolve existing integer-key dictionary entries during recovery
Source recovery cannot reach a child under key 42 in a ReadOnlyDictionary<int, Child>: this guard requires a construction delegate, and the later dictionary fallback looks up the string "42", not the integer key. If Child.Count is zero and suppressed by member-level WhenWritingDefault, a schema-required count is absent from both snapshots and cannot be restored. Resolve existing entries without constructing the runtime dictionary type, preserve serialized-key naming and converter semantics, and add this regression.
[!NOTE]
AI-generated by GitHub Copilot.
Use member converters when recovering default JsonElement values
This shortcut bypasses member converters and runs outside the serialization exception handler. A default JsonElement property decorated with member-level WhenWritingDefault and a converter that writes a valid value for Undefined is omitted from both snapshots. During recovery this branch uses the built-in converter instead, which throws for Undefined, turning the tool call into an error even though the member converter could satisfy the schema. Let values with property metadata use the converter-aware serialization path below.
if (!reference.StartsWith("#", StringComparison.Ordinal))
{
return false;
This branch has not been deployed
No deployments
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
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.
Fixes #1915
Issue: #1915
When structured tool output has an output schema and serializer options ignore null values, required nullable properties are omitted from
structuredContent. Preserve explicit nulls for this serialization path so the result matches its schema while leaving ordinary text serialization unchanged.Add regression coverage for record and class outputs with null and non-null values across the current protocol and
2025-11-25.Validation passed:
dotnet testin Debug and Release across the repository test suites;dotnet format whitespace --verify-no-changes; Windows x64 NativeAOT publish and execution; Release NuGet pack; and DocFX with warnings treated as errors. The Core suites passed 2,398 tests per net8.0/net9.0/net10.0 target (4 skipped) and 2,081 tests on net472 (294 skipped); ASP.NET Core passed 635 tests per modern target (30 skipped); Analyzer passed 57 tests.