Skip to content

Consistent JSON forward/backward compatibility (2.0 foundation)#927

Closed
chemicL wants to merge 6 commits intomainfrom
json-compatibility
Closed

Consistent JSON forward/backward compatibility (2.0 foundation)#927
chemicL wants to merge 6 commits intomainfrom
json-compatibility

Conversation

@chemicL
Copy link
Copy Markdown
Member

@chemicL chemicL commented Apr 20, 2026

Motivation and Context

The MCP specification evolves continuously; domain types must absorb
new fields and subtypes without breaking existing clients or servers.
On the 1.x line this is structurally prevented by sealed interfaces,
which make it impossible to add a permitted subtype without breaking
exhaustive pattern-match switch expressions in caller code. This
commit opens the 2.0 release line, where those constraints are lifted
and serialization is made self-contained — independent of any global
ObjectMapper configuration.

Breaking Changes

Breaking changes for users migrating from 1.x

  • Sealed interfaces removed from JSONRPCMessage, Request, Result,
    Notification, ResourceContents, CompleteReference and Content.
    Exhaustive switch expressions over these types must add a default
    branch.
  • Prompt(name, description, null) no longer silently coerces null
    arguments to an empty list. Use Prompt.withDefaults() to preserve
    the previous behaviour.
  • CompleteCompletion.total and .hasMore are now absent from the wire
    when not set, rather than being emitted as null.
  • ServerParameters no longer carries Jackson annotations; it is an
    internal configuration class, not a wire type.

What now works that did not before

  • CompleteReference polymorphic dispatch (PromptReference vs
    ResourceReference) works through a plain readValue or convertValue
    call — no hand-rolled map inspection required.
  • LoggingLevel deserialization is lenient: unknown level strings
    produce null instead of throwing.
  • All domain records now tolerate unknown JSON fields, so a client
    built against an older SDK version will not fail when a newer server
    sends fields it does not yet recognise.
  • Null optional fields are consistently absent from serialized output
    regardless of ObjectMapper configuration.

Documentation

  • CONTRIBUTING adds an "Evolving wire-serialized records" section: a
    9-rule recipe and example for adding a field safely.
  • MIGRATION-2.0 documents all breaking changes listed above.

Follow-up coming next

Several spec-required fields (e.g. JSONRPCError.code/message,
ProgressNotification.progress, CreateMessageRequest.maxTokens,
CallToolResult.content) are stored as nullable Java types without a
null guard. If constructed with null, the NON_ABSENT rule silently
omits them, producing invalid wire JSON without throwing. Fix: compact
canonical constructors with Assert.notNull, following the pattern
already in JSONRPCRequest.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

chemicL added 2 commits April 20, 2026 17:20
The MCP specification evolves continuously; domain types must absorb
new fields and subtypes without breaking existing clients or servers.
On the 1.x line this is structurally prevented by sealed interfaces,
which make it impossible to add a permitted subtype without breaking
exhaustive pattern-match switch expressions in caller code. This
commit opens the 2.0 release line, where those constraints are lifted
and serialization is made self-contained — independent of any global
ObjectMapper configuration.

Breaking changes for users migrating from 1.x

- Sealed interfaces removed from JSONRPCMessage, Request, Result,
  Notification, ResourceContents, CompleteReference and Content.
  Exhaustive switch expressions over these types must add a default
  branch.
- Prompt(name, description, null) no longer silently coerces null
  arguments to an empty list. Use Prompt.withDefaults() to preserve
  the previous behaviour.
- CompleteCompletion.total and .hasMore are now absent from the wire
  when not set, rather than being emitted as null.
- ServerParameters no longer carries Jackson annotations; it is an
  internal configuration class, not a wire type.

What now works that did not before

- CompleteReference polymorphic dispatch (PromptReference vs
  ResourceReference) works through a plain readValue or convertValue
  call — no hand-rolled map inspection required.
- LoggingLevel deserialization is lenient: unknown level strings
  produce null instead of throwing.
- All domain records now tolerate unknown JSON fields, so a client
  built against an older SDK version will not fail when a newer server
  sends fields it does not yet recognise.
- Null optional fields are consistently absent from serialized output
  regardless of ObjectMapper configuration.

Documentation

- CONTRIBUTING adds an "Evolving wire-serialized records" section: a
  9-rule recipe and example for adding a field safely.
- MIGRATION-2.0 documents all breaking changes listed above.

Follow-up coming next

Several spec-required fields (e.g. JSONRPCError.code/message,
ProgressNotification.progress, CreateMessageRequest.maxTokens,
CallToolResult.content) are stored as nullable Java types without a
null guard. If constructed with null, the NON_ABSENT rule silently
omits them, producing invalid wire JSON without throwing. Fix: compact
canonical constructors with Assert.notNull, following the pattern
already in JSONRPCRequest.

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
chemicL added 3 commits April 20, 2026 18:47
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@tzolov tzolov left a comment

Choose a reason for hiding this comment

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

Please return the @formatter:off for the enum types because combined with the json properties the values are not readable. I've marked few places perhaps there are more.

Additionally the JSONRPCMessage, JSONRPCResponse, JSONRPCError, JSONRPCNotification, JSONRPCRequest and the deserializeJsonRpcMessage are not really MCP Schema related but internal sdk implementation detail and can be moved in a separate class. I guess we can do it in another follow up PR.

@JsonProperty("thisServer") THIS_SERVER,
@JsonProperty("allServers")ALL_SERVERS
} // @formatter:on
@JsonProperty("none")
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.

the @Formatter:off was used on purpose. Without it the content is unreadable.


public enum StopReason {

// @formatter:off
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.

the @Formatter:off was used on purpose. Without it the content is unreadable.


public enum Action {

// @formatter:off
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.

same

"SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");

@JsonProperty("command")
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.

If not mistaken the ServerParameters are modeling the Claude Desktop MCP json configuration (or subset of it) : https://modelcontextprotocol.io/docs/develop/connect-local-servers
Therefore the annotations serve this purpose.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's just a Claude thing, not specification thing. We don't use it to serialize/deserialize and we don't expect that kind of usage. These annotations should be removed.

@tzolov tzolov added this to the 2.0.0-M1 milestone Apr 23, 2026
@tzolov
Copy link
Copy Markdown
Contributor

tzolov commented Apr 23, 2026

Rebased, edited, squashed and merged at 4c85963

@tzolov tzolov closed this Apr 23, 2026
@chemicL
Copy link
Copy Markdown
Member Author

chemicL commented Apr 24, 2026

Please return the @formatter:off for the enum types because combined with the json properties the values are not readable. I've marked few places perhaps there are more.

I didn't catch that. In fact, there is an open issue to address this in the formatter: spring-io/spring-javaformat#393 - would be great to have that.

Additionally the JSONRPCMessage, JSONRPCResponse, JSONRPCError, JSONRPCNotification, JSONRPCRequest and the deserializeJsonRpcMessage are not really MCP Schema related but internal sdk implementation detail and can be moved in a separate class. I guess we can do it in another follow up PR.

That would make sense, I'll try to address it.

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