Consistent JSON forward/backward compatibility (2.0 foundation)#927
Consistent JSON forward/backward compatibility (2.0 foundation)#927
Conversation
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>
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>
tzolov
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
the @Formatter:off was used on purpose. Without it the content is unreadable.
|
|
||
| public enum StopReason { | ||
|
|
||
| // @formatter:off |
There was a problem hiding this comment.
the @Formatter:off was used on purpose. Without it the content is unreadable.
|
|
||
| public enum Action { | ||
|
|
||
| // @formatter:off |
| "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE") | ||
| : Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"); | ||
|
|
||
| @JsonProperty("command") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Rebased, edited, squashed and merged at 4c85963 |

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
Notification, ResourceContents, CompleteReference and Content.
Exhaustive switch expressions over these types must add a default
branch.
arguments to an empty list. Use Prompt.withDefaults() to preserve
the previous behaviour.
when not set, rather than being emitted as null.
internal configuration class, not a wire type.
What now works that did not before
ResourceReference) works through a plain readValue or convertValue
call — no hand-rolled map inspection required.
produce null instead of throwing.
built against an older SDK version will not fail when a newer server
sends fields it does not yet recognise.
regardless of ObjectMapper configuration.
Documentation
9-rule recipe and example for adding a field safely.
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
Checklist