Skip to content

Commit 7c01d9f

Browse files
committed
union based deserialization
1 parent 2bceacf commit 7c01d9f

7 files changed

Lines changed: 100 additions & 26 deletions

File tree

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
9696
[JsonSerializable(typeof(JsonRpcNotification))]
9797
[JsonSerializable(typeof(JsonRpcResponse))]
9898
[JsonSerializable(typeof(JsonRpcError))]
99+
100+
// JSON-RPC union to make it faster to deserialize messages
101+
[JsonSerializable(typeof(JsonRpcMessage.Converter.Union))]
99102

100103
// MCP Notification Params
101104
[JsonSerializable(typeof(CancelledNotificationParams))]

src/ModelContextProtocol.Core/Protocol/JsonRpcError.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ namespace ModelContextProtocol.Protocol;
1818
/// </remarks>
1919
public sealed class JsonRpcError : JsonRpcMessageWithId
2020
{
21+
internal const string ErrorPropertyName = "error";
22+
2123
/// <summary>
2224
/// Gets detailed error information for the failed request, containing an error code,
2325
/// message, and optional additional data
2426
/// </summary>
25-
[JsonPropertyName("error")]
27+
[JsonPropertyName(ErrorPropertyName)]
2628
public required JsonRpcErrorDetail Error { get; init; }
2729
}

src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs

Lines changed: 78 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using ModelContextProtocol.Server;
22
using System.ComponentModel;
33
using System.Text.Json;
4+
using System.Text.Json.Nodes;
45
using System.Text.Json.Serialization;
56

67
namespace ModelContextProtocol.Protocol;
@@ -16,6 +17,8 @@ namespace ModelContextProtocol.Protocol;
1617
[JsonConverter(typeof(Converter))]
1718
public abstract class JsonRpcMessage
1819
{
20+
private const string JsonRpcPropertyName = "jsonrpc";
21+
1922
/// <summary>Prevent external derivations.</summary>
2023
private protected JsonRpcMessage()
2124
{
@@ -25,7 +28,7 @@ private protected JsonRpcMessage()
2528
/// Gets the JSON-RPC protocol version used.
2629
/// </summary>
2730
/// <inheritdoc />
28-
[JsonPropertyName("jsonrpc")]
31+
[JsonPropertyName(JsonRpcPropertyName)]
2932
public string JsonRpc { get; init; } = "2.0";
3033

3134
/// <summary>
@@ -75,6 +78,48 @@ private protected JsonRpcMessage()
7578
[EditorBrowsable(EditorBrowsableState.Never)]
7679
public sealed class Converter : JsonConverter<JsonRpcMessage>
7780
{
81+
/// <summary>
82+
/// The union to deserialize.
83+
/// </summary>
84+
public struct Union
85+
{
86+
/// <summary>
87+
/// <see cref="JsonRpcMessage.JsonRpc"/>
88+
/// </summary>
89+
[JsonPropertyName(JsonRpcPropertyName)]
90+
public string JsonRpc { get; set; }
91+
92+
/// <summary>
93+
/// <see cref="JsonRpcMessageWithId.Id"/>
94+
/// </summary>
95+
[JsonPropertyName(JsonRpcMessageWithId.IdPropertyName)]
96+
public RequestId Id { get; set; }
97+
98+
/// <summary>
99+
/// <see cref="JsonRpcRequest.Method"/>
100+
/// </summary>
101+
[JsonPropertyName(JsonRpcRequest.MethodPropertyName)]
102+
public string? Method { get; set; }
103+
104+
/// <summary>
105+
/// <see cref="JsonRpcRequest.Params"/>
106+
/// </summary>
107+
[JsonPropertyName(JsonRpcRequest.ParamsPropertyName)]
108+
public JsonNode? Params { get; set; }
109+
110+
/// <summary>
111+
/// <see cref="JsonRpcError.Error"/>
112+
/// </summary>
113+
[JsonPropertyName(JsonRpcError.ErrorPropertyName)]
114+
public JsonRpcErrorDetail? Error { get; set; }
115+
116+
/// <summary>
117+
/// <see cref="JsonRpcResponse.Result"/>
118+
/// </summary>
119+
[JsonPropertyName(JsonRpcResponse.ResultPropertyName)]
120+
public JsonNode? Result { get; set; }
121+
}
122+
78123
/// <inheritdoc/>
79124
public override JsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
80125
{
@@ -83,51 +128,63 @@ public sealed class Converter : JsonConverter<JsonRpcMessage>
83128
throw new JsonException("Expected StartObject token");
84129
}
85130

86-
using var doc = JsonDocument.ParseValue(ref reader);
87-
var root = doc.RootElement;
131+
var union = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<Union>());
88132

89133
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
90-
if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
91-
versionProperty.GetString() != "2.0")
134+
if (union.JsonRpc != "2.0")
92135
{
93136
throw new JsonException("Invalid or missing jsonrpc version");
94137
}
95138

96-
// Determine the message type based on the presence of id, method, and error properties
97-
bool hasId = root.TryGetProperty("id", out _);
98-
bool hasMethod = root.TryGetProperty("method", out _);
99-
bool hasError = root.TryGetProperty("error", out _);
100-
101-
var rawText = root.GetRawText();
102-
103139
// Messages with an id but no method are responses
104-
if (hasId && !hasMethod)
140+
if (union.Id.HasValue && union.Method is null)
105141
{
106142
// Messages with an error property are error responses
107-
if (hasError)
143+
if (union.Error != null)
108144
{
109-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>());
145+
return new JsonRpcError
146+
{
147+
Id = union.Id,
148+
Error = union.Error,
149+
JsonRpc = union.JsonRpc,
150+
};
110151
}
111152

112153
// Messages with a result property are success responses
113-
if (root.TryGetProperty("result", out _))
154+
if (union.Result != null)
114155
{
115-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcResponse>());
156+
return new JsonRpcResponse
157+
{
158+
Id = union.Id,
159+
Result = union.Result,
160+
JsonRpc = union.JsonRpc,
161+
};
116162
}
117163

118164
throw new JsonException("Response must have either result or error");
119165
}
120166

121167
// Messages with a method but no id are notifications
122-
if (hasMethod && !hasId)
168+
if (union.Method != null && !union.Id.HasValue)
123169
{
124-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcNotification>());
170+
return new JsonRpcNotification
171+
{
172+
Method = union.Method,
173+
JsonRpc = union.JsonRpc,
174+
Params = union.Params,
175+
};
125176
}
126177

127178
// Messages with both method and id are requests
128-
if (hasMethod && hasId)
179+
if (union.Method != null && union.Id.HasValue)
129180
{
130-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcRequest>());
181+
return new JsonRpcRequest
182+
{
183+
Id = union.Id,
184+
Method = union.Method,
185+
JsonRpc = union.JsonRpc,
186+
Params = union.Params,
187+
};
131188
}
132189

133190
throw new JsonException("Invalid JSON-RPC message format");

src/ModelContextProtocol.Core/Protocol/JsonRpcMessageWithId.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ namespace ModelContextProtocol.Protocol;
1414
/// </remarks>
1515
public abstract class JsonRpcMessageWithId : JsonRpcMessage
1616
{
17+
internal const string IdPropertyName = "id";
18+
1719
/// <summary>Prevent external derivations.</summary>
1820
private protected JsonRpcMessageWithId()
1921
{
@@ -25,6 +27,6 @@ private protected JsonRpcMessageWithId()
2527
/// <remarks>
2628
/// Each ID is expected to be unique within the context of a given session.
2729
/// </remarks>
28-
[JsonPropertyName("id")]
30+
[JsonPropertyName(IdPropertyName)]
2931
public RequestId Id { get; init; }
3032
}

src/ModelContextProtocol.Core/Protocol/JsonRpcRequest.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@ namespace ModelContextProtocol.Protocol;
1616
/// </remarks>
1717
public sealed class JsonRpcRequest : JsonRpcMessageWithId
1818
{
19+
internal const string MethodPropertyName = "method";
20+
internal const string ParamsPropertyName = "params";
21+
1922
/// <summary>
2023
/// Name of the method to invoke.
2124
/// </summary>
22-
[JsonPropertyName("method")]
25+
[JsonPropertyName(MethodPropertyName)]
2326
public required string Method { get; init; }
2427

2528
/// <summary>
2629
/// Optional parameters for the method.
2730
/// </summary>
28-
[JsonPropertyName("params")]
31+
[JsonPropertyName(ParamsPropertyName)]
2932
public JsonNode? Params { get; init; }
3033

3134
internal JsonRpcRequest WithId(RequestId id)

src/ModelContextProtocol.Core/Protocol/JsonRpcResponse.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ namespace ModelContextProtocol.Protocol;
1818
/// </remarks>
1919
public sealed class JsonRpcResponse : JsonRpcMessageWithId
2020
{
21+
internal const string ResultPropertyName = "result";
22+
2123
/// <summary>
2224
/// Gets the result of the method invocation.
2325
/// </summary>
2426
/// <remarks>
2527
/// This property contains the result data returned by the server in response to the JSON-RPC method request.
2628
/// </remarks>
27-
[JsonPropertyName("result")]
29+
[JsonPropertyName(ResultPropertyName)]
2830
public required JsonNode? Result { get; init; }
2931
}

src/ModelContextProtocol.Core/Protocol/RequestId.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public RequestId(long value)
3434
/// <remarks>This will either be a <see cref="string"/>, a boxed <see cref="long"/>, or <see langword="null"/>.</remarks>
3535
public object? Id => _id;
3636

37+
/// <summary>
38+
/// Returns true if the underlying id is set.
39+
/// </summary>
40+
public bool HasValue => _id != null;
41+
3742
/// <inheritdoc />
3843
public override string ToString() =>
3944
_id is string stringValue ? stringValue :

0 commit comments

Comments
 (0)