Repository navigation
Preserve nulls required by structured output schemas - #1929
mikemikimike wants to merge 1 commit into
Conversation
|
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 (
Both clients validate Until this ships, the per-member workaround that worked for me is |
tarekgh
left a comment
There was a problem hiding this comment.
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.
| function.JsonSerializerOptions.DefaultIgnoreCondition is JsonIgnoreCondition.WhenWritingNull | ||
| ? new JsonSerializerOptions(function.JsonSerializerOptions) | ||
| { | ||
| DefaultIgnoreCondition = JsonIgnoreCondition.Never, |
There was a problem hiding this comment.
Could we preserve the previously valid behavior for explicit output schemas?
For example, an explicit schema can make note optional but require a string when it is present:
{
"type": "object",
"properties": {
"id": { "type": "string" },
"note": { "type": "string" }
},
"required": ["id"]
}With a null Note, WhenWritingNull previously produced {"id":"42"}, which satisfies this schema. Changing the global setting to Never produces {"id":"42","note":null}, which fails validation.
Could we add a regression test and avoid forcing nulls into properties where the explicit schema does not permit them?
| [InlineData("note", Sep2106ProtocolVersion)] | ||
| [InlineData(null, "2025-11-25")] | ||
| [InlineData("note", "2025-11-25")] | ||
| public async Task StructuredOutput_Enabled_RequiredNullableProperties_MatchesOutputSchema(string? note, string protocolVersion) |
There was a problem hiding this comment.
Could we also cover a required nullable member with DefaultIgnoreCondition = WhenWritingDefault, and one annotated with [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]?
The first bypasses the new condition, while the second overrides the global Never setting. Both can still omit a member required by the generated schema. These are pre-existing gaps rather than regressions introduced here, but coverage would help establish whether they should be handled or documented as limitations.
There was a problem hiding this comment.
🟡 Changes recommended
Globally including nulls can invalidate previously valid explicit-schema responses whose optional properties prohibit null.
1 open finding
What changed in this PR
Addresses #1915 by preserving nulls in structured tool results to match required nullable output properties.
Changes:
- Adds separate serializer options for schema-backed structured content, leaving text serialization unchanged.
- Adds record and class regression tests across two protocol versions.
| File | Description |
|---|---|
| tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs | Tests nullable output properties against their schemas. |
| src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs | Preserves nulls during schema-backed structured serialization. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| function.JsonSerializerOptions.DefaultIgnoreCondition is JsonIgnoreCondition.WhenWritingNull | ||
| ? new JsonSerializerOptions(function.JsonSerializerOptions) | ||
| { | ||
| DefaultIgnoreCondition = JsonIgnoreCondition.Never, |
There was a problem hiding this comment.
@copilot isn't the same issue raised in the comment #1929 (comment)?

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.