Skip to content

Commit 9f01d2d

Browse files
Optimize JsonRpcMessage.Converter.Read with single-pass deserialization
- Add private Union struct to hold parsed message data - Implement hand-crafted Parse method for single-pass JSON reading - Replace double-deserialization pattern with direct object construction - Use GetTypeInfo for AOT compatibility - All tests pass (1019 tests in ModelContextProtocol.Tests) Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
1 parent 750c507 commit 9f01d2d

File tree

1 file changed

+116
-24
lines changed

1 file changed

+116
-24
lines changed

src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs

Lines changed: 116 additions & 24 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;
@@ -70,59 +71,150 @@ private protected JsonRpcMessage()
7071
[EditorBrowsable(EditorBrowsableState.Never)]
7172
public sealed class Converter : JsonConverter<JsonRpcMessage>
7273
{
73-
/// <inheritdoc/>
74-
public override JsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
74+
/// <summary>
75+
/// Private struct to hold parsed JSON-RPC message data during deserialization.
76+
/// </summary>
77+
private struct Union
7578
{
76-
if (reader.TokenType != JsonTokenType.StartObject)
79+
public string JsonRpc;
80+
public RequestId Id;
81+
public string? Method;
82+
public JsonNode? Params;
83+
public JsonRpcErrorDetail? Error;
84+
public JsonNode? Result;
85+
public bool HasId;
86+
public bool HasMethod;
87+
public bool HasError;
88+
public bool HasResult;
89+
90+
/// <summary>
91+
/// Manually parses a JSON-RPC message from the reader into the Union struct.
92+
/// </summary>
93+
public static Union Parse(ref Utf8JsonReader reader, JsonSerializerOptions options)
7794
{
78-
throw new JsonException("Expected StartObject token");
95+
var union = new Union();
96+
97+
if (reader.TokenType != JsonTokenType.StartObject)
98+
{
99+
throw new JsonException("Expected StartObject token");
100+
}
101+
102+
while (reader.Read())
103+
{
104+
if (reader.TokenType == JsonTokenType.EndObject)
105+
{
106+
break;
107+
}
108+
109+
if (reader.TokenType != JsonTokenType.PropertyName)
110+
{
111+
throw new JsonException("Expected PropertyName token");
112+
}
113+
114+
string propertyName = reader.GetString()!;
115+
reader.Read();
116+
117+
switch (propertyName)
118+
{
119+
case "jsonrpc":
120+
union.JsonRpc = reader.GetString()!;
121+
break;
122+
123+
case "id":
124+
union.Id = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<RequestId>());
125+
union.HasId = true;
126+
break;
127+
128+
case "method":
129+
union.Method = reader.GetString();
130+
union.HasMethod = true;
131+
break;
132+
133+
case "params":
134+
union.Params = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonNode>());
135+
break;
136+
137+
case "error":
138+
union.Error = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonRpcErrorDetail>());
139+
union.HasError = true;
140+
break;
141+
142+
case "result":
143+
union.Result = JsonSerializer.Deserialize(ref reader, options.GetTypeInfo<JsonNode>());
144+
union.HasResult = true;
145+
break;
146+
147+
default:
148+
// Skip unknown properties
149+
reader.Skip();
150+
break;
151+
}
152+
}
153+
154+
return union;
79155
}
156+
}
80157

81-
using var doc = JsonDocument.ParseValue(ref reader);
82-
var root = doc.RootElement;
158+
/// <inheritdoc/>
159+
public override JsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
160+
{
161+
var union = Union.Parse(ref reader, options);
83162

84163
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
85-
if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
86-
versionProperty.GetString() != "2.0")
164+
if (union.JsonRpc != "2.0")
87165
{
88166
throw new JsonException("Invalid or missing jsonrpc version");
89167
}
90168

91-
// Determine the message type based on the presence of id, method, and error properties
92-
bool hasId = root.TryGetProperty("id", out _);
93-
bool hasMethod = root.TryGetProperty("method", out _);
94-
bool hasError = root.TryGetProperty("error", out _);
95-
96-
var rawText = root.GetRawText();
97-
98169
// Messages with an id but no method are responses
99-
if (hasId && !hasMethod)
170+
if (union.HasId && !union.HasMethod)
100171
{
101172
// Messages with an error property are error responses
102-
if (hasError)
173+
if (union.HasError)
103174
{
104-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>());
175+
return new JsonRpcError
176+
{
177+
JsonRpc = union.JsonRpc,
178+
Id = union.Id,
179+
Error = union.Error!
180+
};
105181
}
106182

107183
// Messages with a result property are success responses
108-
if (root.TryGetProperty("result", out _))
184+
if (union.HasResult)
109185
{
110-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcResponse>());
186+
return new JsonRpcResponse
187+
{
188+
JsonRpc = union.JsonRpc,
189+
Id = union.Id,
190+
Result = union.Result
191+
};
111192
}
112193

113194
throw new JsonException("Response must have either result or error");
114195
}
115196

116197
// Messages with a method but no id are notifications
117-
if (hasMethod && !hasId)
198+
if (union.HasMethod && !union.HasId)
118199
{
119-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcNotification>());
200+
return new JsonRpcNotification
201+
{
202+
JsonRpc = union.JsonRpc,
203+
Method = union.Method!,
204+
Params = union.Params
205+
};
120206
}
121207

122208
// Messages with both method and id are requests
123-
if (hasMethod && hasId)
209+
if (union.HasMethod && union.HasId)
124210
{
125-
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcRequest>());
211+
return new JsonRpcRequest
212+
{
213+
JsonRpc = union.JsonRpc,
214+
Id = union.Id,
215+
Method = union.Method!,
216+
Params = union.Params
217+
};
126218
}
127219

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

0 commit comments

Comments
 (0)