Skip to content

Preserve nulls required by structured output schemas - #1929

Open
mikemikimike wants to merge 1 commit into
modelcontextprotocol:mainfrom
mikemikimike:fix/structured-content-null-schema
Open

mikemikimike wants to merge 1 commit into
modelcontextprotocol:mainfrom
mikemikimike:fix/structured-content-null-schema

Conversation

@mikemikimike

Copy link
Copy Markdown

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 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.

@PederHP

PederHP commented Oct 14, 2026

Copy link
Copy Markdown
Member

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:

Client Server on ModelContextProtocol 2.2.0 Same server built against this PR (519e96c)
Python mcp 2.0.0 (negotiates 2026-07-28) Invalid structured content returned by tool check_operation_status: 'result' is a required property OK, "result": null
TypeScript @modelcontextprotocol/client 2.2.0 (versionNegotiation: { mode: 'auto' }) 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.

@tarekgh tarekgh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

function.JsonSerializerOptions.DefaultIgnoreCondition is JsonIgnoreCondition.WhenWritingNull
? new JsonSerializerOptions(function.JsonSerializerOptions)
{
DefaultIgnoreCondition = JsonIgnoreCondition.Never,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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,

@tarekgh tarekgh Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@copilot isn't the same issue raised in the comment #1929 (comment)?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The previously reported regression remains unresolved for explicit schemas whose optional properties prohibit null values.

1 open finding

🧠 Review effort: Balanced

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UseStructuredContent: with the default serializer options, structuredContent omits a null property that outputSchema requires

4 participants