Skip to content

Commit 772fcd5

Browse files
committed
- implement Jackson 2 parser + tests
1 parent eaee4fa commit 772fcd5

File tree

9 files changed

+349
-9
lines changed

9 files changed

+349
-9
lines changed

jooby/src/main/java/io/jooby/jsonrpc/JsonRpcModule.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import io.jooby.*;
1414
import io.jooby.exception.MissingValueException;
15+
import io.jooby.exception.TypeMismatchException;
1516

1617
/**
1718
* Global Tier 1 Dispatcher for JSON-RPC 2.0 requests. *
@@ -69,7 +70,8 @@ public void install(Jooby app) throws Exception {
6970
// Initialize the custom exception mapping registry
7071
app.getServices()
7172
.mapOf(Class.class, JsonRpcErrorCode.class)
72-
.put(MissingValueException.class, JsonRpcErrorCode.INVALID_PARAMS);
73+
.put(MissingValueException.class, JsonRpcErrorCode.INVALID_PARAMS)
74+
.put(TypeMismatchException.class, JsonRpcErrorCode.INVALID_PARAMS);
7375
}
7476

7577
/**
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jackson;
7+
8+
import java.lang.reflect.Type;
9+
10+
import com.fasterxml.jackson.databind.JavaType;
11+
import com.fasterxml.jackson.databind.JsonNode;
12+
import com.fasterxml.jackson.databind.ObjectMapper;
13+
import io.jooby.jsonrpc.JsonRpcDecoder;
14+
import io.jooby.jsonrpc.JsonRpcErrorCode;
15+
import io.jooby.jsonrpc.JsonRpcException;
16+
17+
public class JacksonJsonRpcDecoder<T> implements JsonRpcDecoder<T> {
18+
19+
private final ObjectMapper mapper;
20+
private final JavaType javaType;
21+
22+
public JacksonJsonRpcDecoder(ObjectMapper mapper, Type type) {
23+
this.mapper = mapper;
24+
this.javaType = mapper.constructType(type);
25+
}
26+
27+
@Override
28+
public T decode(String name, Object node) {
29+
try {
30+
if (node == null || ((JsonNode) node).isNull()) {
31+
return null;
32+
}
33+
return mapper.treeToValue((JsonNode) node, javaType);
34+
} catch (Exception x) {
35+
throw new JsonRpcException(
36+
JsonRpcErrorCode.INVALID_PARAMS,
37+
"Invalid params: unable to map parameter '" + name + "'",
38+
x);
39+
}
40+
}
41+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jackson;
7+
8+
import java.lang.reflect.Type;
9+
10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import io.jooby.jsonrpc.JsonRpcDecoder;
13+
import io.jooby.jsonrpc.JsonRpcParser;
14+
import io.jooby.jsonrpc.JsonRpcReader;
15+
16+
public class JacksonJsonRpcParser implements JsonRpcParser {
17+
18+
private final ObjectMapper mapper;
19+
20+
public JacksonJsonRpcParser(ObjectMapper mapper) {
21+
this.mapper = mapper;
22+
}
23+
24+
@Override
25+
public <T> JsonRpcDecoder<T> decoder(Type type) {
26+
return new JacksonJsonRpcDecoder<>(mapper, type);
27+
}
28+
29+
@Override
30+
public JsonRpcReader reader(Object params) {
31+
// The JsonRpcRequestDeserializer stores the params field as a JsonNode
32+
JsonNode node = (JsonNode) params;
33+
return new JacksonJsonRpcReader(node);
34+
}
35+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jackson;
7+
8+
import com.fasterxml.jackson.databind.JsonNode;
9+
import io.jooby.exception.MissingValueException;
10+
import io.jooby.exception.TypeMismatchException;
11+
import io.jooby.jsonrpc.JsonRpcDecoder;
12+
import io.jooby.jsonrpc.JsonRpcReader;
13+
14+
public class JacksonJsonRpcReader implements JsonRpcReader {
15+
16+
private final JsonNode params;
17+
private final boolean isArray;
18+
private int index = 0;
19+
20+
public JacksonJsonRpcReader(JsonNode params) {
21+
this.params = params;
22+
this.isArray = params != null && params.isArray();
23+
}
24+
25+
private JsonNode peekNode(String name) {
26+
if (params == null) {
27+
return null;
28+
}
29+
if (isArray) {
30+
return params.get(index);
31+
} else if (params.isObject()) {
32+
return params.get(name);
33+
}
34+
return null;
35+
}
36+
37+
private JsonNode consumeNode(String name) {
38+
if (params == null) {
39+
return null;
40+
}
41+
if (isArray) {
42+
return params.get(index++);
43+
} else if (params.isObject()) {
44+
return params.get(name);
45+
}
46+
return null;
47+
}
48+
49+
private JsonNode requireNode(String name) {
50+
JsonNode node = consumeNode(name);
51+
if (node == null || node.isNull() || node.isMissingNode()) {
52+
throw new MissingValueException(name);
53+
}
54+
return node;
55+
}
56+
57+
@Override
58+
public boolean nextIsNull(String name) {
59+
JsonNode node = peekNode(name);
60+
return node == null || node.isNull();
61+
}
62+
63+
@Override
64+
public int nextInt(String name) {
65+
var node = requireNode(name);
66+
if (node.isNumber()) {
67+
return node.intValue();
68+
}
69+
throw new TypeMismatchException(name, int.class);
70+
}
71+
72+
@Override
73+
public long nextLong(String name) {
74+
var node = requireNode(name);
75+
if (node.isNumber()) {
76+
return node.longValue();
77+
}
78+
throw new TypeMismatchException(name, long.class);
79+
}
80+
81+
@Override
82+
public boolean nextBoolean(String name) {
83+
var node = requireNode(name);
84+
if (node.isBoolean()) {
85+
return node.booleanValue();
86+
}
87+
throw new TypeMismatchException(name, boolean.class);
88+
}
89+
90+
@Override
91+
public double nextDouble(String name) {
92+
var node = requireNode(name);
93+
if (node.isNumber()) {
94+
return node.doubleValue();
95+
}
96+
throw new TypeMismatchException(name, double.class);
97+
}
98+
99+
@Override
100+
public String nextString(String name) {
101+
var node = requireNode(name);
102+
if (node.isTextual()) {
103+
return node.textValue();
104+
}
105+
throw new TypeMismatchException(name, String.class);
106+
}
107+
108+
@Override
109+
public <T> T nextObject(String name, JsonRpcDecoder<T> decoder) {
110+
JsonNode node = requireNode(name);
111+
return decoder.decode(name, node);
112+
}
113+
114+
@Override
115+
public void close() {
116+
// No network resources to release for an in-memory JsonNode tree
117+
}
118+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.jackson;
7+
8+
import java.io.IOException;
9+
10+
import com.fasterxml.jackson.core.JsonParser;
11+
import com.fasterxml.jackson.databind.DeserializationContext;
12+
import com.fasterxml.jackson.databind.JsonNode;
13+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
14+
import com.fasterxml.jackson.databind.node.ArrayNode;
15+
import io.jooby.jsonrpc.JsonRpcRequest;
16+
17+
public class JacksonJsonRpcRequestDeserializer extends StdDeserializer<JsonRpcRequest> {
18+
19+
public JacksonJsonRpcRequestDeserializer() {
20+
super(JsonRpcRequest.class);
21+
}
22+
23+
@Override
24+
public JsonRpcRequest deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
25+
// Jackson 2 standard way to read the tree
26+
JsonNode node = p.getCodec().readTree(p);
27+
28+
if (node.isArray()) {
29+
var arrayNode = (ArrayNode) node;
30+
31+
// Spec: Empty array must return a single Invalid Request object (-32600)
32+
if (arrayNode.isEmpty()) {
33+
JsonRpcRequest invalid = new JsonRpcRequest();
34+
invalid.setMethod(null); // Acts as a flag for Invalid Request
35+
invalid.setBatch(false); // Force single return shape
36+
return invalid;
37+
}
38+
39+
JsonRpcRequest batch = new JsonRpcRequest();
40+
for (JsonNode element : arrayNode) {
41+
batch.add(parseSingle(element));
42+
}
43+
return batch;
44+
} else {
45+
return parseSingle(node);
46+
}
47+
}
48+
49+
private JsonRpcRequest parseSingle(JsonNode node) {
50+
var req = new JsonRpcRequest();
51+
52+
if (!node.isObject()) {
53+
req.setMethod(null);
54+
return req;
55+
}
56+
57+
// 1. Extract ID if present (crucial for error echoing)
58+
JsonNode idNode = node.get("id");
59+
if (idNode != null && !idNode.isNull()) {
60+
if (idNode.isNumber()) {
61+
req.setId(idNode.numberValue());
62+
} else if (idNode.isTextual()) { // Jackson 2 uses isTextual()
63+
req.setId(idNode.asText()); // Jackson 2 uses asText() instead of asString()
64+
}
65+
}
66+
67+
// 2. Validate JSON-RPC version
68+
JsonNode versionNode = node.get("jsonrpc");
69+
if (versionNode == null || !versionNode.isTextual() || !"2.0".equals(versionNode.asText())) {
70+
req.setMethod(null); // Triggers -32600 Invalid Request
71+
return req;
72+
}
73+
74+
// 3. Extract Method
75+
JsonNode methodNode = node.get("method");
76+
if (methodNode != null && methodNode.isTextual()) {
77+
req.setMethod(methodNode.asText());
78+
} else {
79+
req.setMethod(null); // Triggers -32600 Invalid Request
80+
}
81+
82+
// 4. Extract Params (Must be an Array or an Object per spec)
83+
JsonNode paramsNode = node.get("params");
84+
if (paramsNode != null && !paramsNode.isNull()) {
85+
if (paramsNode.isArray() || paramsNode.isObject()) {
86+
req.setParams(paramsNode); // Keep as JsonNode for the Reader later
87+
} else {
88+
req.setMethod(null); // Primitive params are invalid -> -32600
89+
}
90+
}
91+
92+
return req;
93+
}
94+
}

modules/jooby-jackson/src/main/java/io/jooby/jackson/JacksonModule.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@
3030
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
3131
import edu.umd.cs.findbugs.annotations.NonNull;
3232
import io.jooby.*;
33+
import io.jooby.internal.jackson.JacksonJsonRpcParser;
34+
import io.jooby.internal.jackson.JacksonJsonRpcRequestDeserializer;
3335
import io.jooby.internal.jackson.JacksonTrpcParser;
3436
import io.jooby.internal.jackson.JacksonTrpcResponseSerializer;
37+
import io.jooby.jsonrpc.JsonRpcErrorCode;
38+
import io.jooby.jsonrpc.JsonRpcParser;
39+
import io.jooby.jsonrpc.JsonRpcRequest;
3540
import io.jooby.output.Output;
3641
import io.jooby.trpc.TrpcParser;
3742
import io.jooby.trpc.TrpcResponse;
@@ -157,6 +162,13 @@ public void install(@NonNull Jooby application) {
157162
// tRPC
158163
services.put(TrpcParser.class, new JacksonTrpcParser(mapper));
159164

165+
// JSON-RPC
166+
services.put(JsonRpcParser.class, new JacksonJsonRpcParser(mapper));
167+
services
168+
.mapOf(Class.class, JsonRpcErrorCode.class)
169+
.put(MismatchedInputException.class, JsonRpcErrorCode.INVALID_PARAMS)
170+
.put(DatabindException.class, JsonRpcErrorCode.INVALID_PARAMS);
171+
160172
// Filter
161173
var defaultProvider = new SimpleFilterProvider().setFailOnUnknownId(false);
162174
mapper.addMixIn(Object.class, ProjectionMixIn.class);
@@ -220,18 +232,19 @@ public Object decode(Context ctx, Type type) throws Exception {
220232
* @param modules Extra/additional modules to install.
221233
* @return Object mapper instance.
222234
*/
223-
public static @NonNull ObjectMapper create(Module... modules) {
235+
public static ObjectMapper create(Module... modules) {
224236
JsonMapper.Builder builder =
225237
JsonMapper.builder()
226238
.addModule(new ParameterNamesModule())
227239
.addModule(new Jdk8Module())
228240
.addModule(new JavaTimeModule());
229241

230242
Stream.of(modules).forEach(builder::addModule);
231-
// tRPC
232-
var trpcModule = new SimpleModule();
233-
trpcModule.addSerializer(TrpcResponse.class, new JacksonTrpcResponseSerializer());
234-
builder.addModule(trpcModule);
243+
// RPC
244+
var rpc = new SimpleModule();
245+
rpc.addSerializer(TrpcResponse.class, new JacksonTrpcResponseSerializer());
246+
rpc.addDeserializer(JsonRpcRequest.class, new JacksonJsonRpcRequestDeserializer());
247+
builder.addModule(rpc);
235248

236249
return builder.build();
237250
}

tests/src/test/java/io/jooby/i3868/JsonRpcProtocolTest.java renamed to tests/src/test/java/io/jooby/i3868/AbstractJsonRpcProtocolTest.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111

1212
import com.jayway.jsonpath.JsonPath;
1313
import io.jooby.Jooby;
14-
import io.jooby.jackson3.Jackson3Module;
1514
import io.jooby.junit.ServerTest;
1615
import io.jooby.junit.ServerTestRunner;
1716

18-
public class JsonRpcProtocolTest {
17+
public abstract class AbstractJsonRpcProtocolTest {
18+
19+
/**
20+
* Subclasses must provide the specific JSON engine module (e.g., JacksonModule, Jackson3Module).
21+
*/
22+
protected abstract void installJsonEngine(Jooby app);
1923

2024
// Helper to keep test setup DRY
2125
private void setupApp(Jooby app) {
22-
app.install(new Jackson3Module());
26+
installJsonEngine(app);
2327
app.mvc(new MovieService_());
2428
app.mvc(new MovieServiceRpc_());
2529
}
@@ -198,6 +202,7 @@ void shouldHandleInvalidParams(ServerTestRunner runner) {
198202
""",
199203
rsp -> {
200204
String json = rsp.body().string();
205+
System.out.println(json);
201206
assertThat(rsp.code()).isEqualTo(200);
202207
assertThat(JsonPath.<Integer>read(json, "$.error.code")).isEqualTo(-32602);
203208
});

0 commit comments

Comments
 (0)