Skip to content

Commit 8e020a1

Browse files
wenshaoclaude
andcommitted
feat: add JSONPath query engine (RFC 9535 Phase 1)
Core JSONPath implementation with tree-mode evaluation: - Compiler: $, .name, ['name'], [index], [-index], [*], [start:end:step], [0,2], ['a','b'], ..name, ..[*] (recursive descent) - Filter expressions: comparison (==,!=,<,<=,>,>=), logical (&&,||,!), existence tests, path expressions (@.name, $.root), literals - Specialized fast paths: SingleNamePath ($.name), TwoNamePath ($.a.b) - Definite vs indefinite path semantics - Type conversion in eval() - Static convenience: JSON.eval(json, path, type) - 24 tests covering all syntax features Architecture: JSONPathCompiler → JSONPathSegment[] → JSONPathContext eval Segment types: Name, Index, Wildcard, Slice, MultiIndex, MultiName, RecursiveDescent, Filter Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 513454c commit 8e020a1

7 files changed

Lines changed: 1521 additions & 0 deletions

File tree

core3/src/main/java/com/alibaba/fastjson3/JSON.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,17 @@ public static JSONArray array(Object... items) {
253253
}
254254

255255
private static final byte[] NULL_BYTES = {'n', 'u', 'l', 'l'};
256+
257+
// ==================== JSONPath ====================
258+
259+
/**
260+
* Evaluate a JSONPath expression on a JSON string.
261+
*
262+
* <pre>
263+
* String title = JSON.eval(json, "$.store.book[0].title", String.class);
264+
* </pre>
265+
*/
266+
public static <T> T eval(String json, String path, Class<T> type) {
267+
return JSONPath.eval(json, path, type);
268+
}
256269
}
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
package com.alibaba.fastjson3;
2+
3+
import com.alibaba.fastjson3.jsonpath.JSONPathCompiler;
4+
import com.alibaba.fastjson3.jsonpath.JSONPathContext;
5+
import com.alibaba.fastjson3.jsonpath.JSONPathSegment;
6+
7+
import java.util.Map;
8+
9+
/**
10+
* JSONPath query engine for fastjson3 (RFC 9535 compatible).
11+
*
12+
* <p>Compiles path expressions into reusable, thread-safe query objects.
13+
* Supports tree-mode evaluation on JSONObject/JSONArray and streaming extraction
14+
* from JSON strings/bytes.</p>
15+
*
16+
* <h3>Basic usage:</h3>
17+
* <pre>
18+
* // Compile once, reuse many times
19+
* JSONPath path = JSONPath.of("$.store.book[0].title");
20+
*
21+
* // Evaluate on a parsed object
22+
* String title = path.eval(jsonObject, String.class);
23+
*
24+
* // Extract directly from JSON string (parse + eval)
25+
* String title = path.extract("{...}", String.class);
26+
*
27+
* // Static convenience
28+
* String title = JSONPath.eval("{...}", "$.store.book[0].title", String.class);
29+
* </pre>
30+
*
31+
* <h3>Supported syntax (RFC 9535):</h3>
32+
* <pre>
33+
* $ root
34+
* $.name or $['name'] child property
35+
* $[0], $[-1] array index
36+
* $[*] wildcard
37+
* $[1:3], $[::2] array slice
38+
* $..name recursive descent
39+
* $[?&#64;.price &lt; 10] filter expression
40+
* </pre>
41+
*/
42+
public abstract sealed class JSONPath {
43+
final boolean definite;
44+
45+
JSONPath(boolean definite) {
46+
this.definite = definite;
47+
}
48+
49+
// ==================== Factory ====================
50+
51+
/**
52+
* Compile a JSONPath expression. The returned object is immutable and thread-safe.
53+
* Users should cache compiled paths for repeated use.
54+
*
55+
* @param path the JSONPath expression (e.g., "$.store.book[*].author")
56+
* @return a compiled JSONPath
57+
*/
58+
public static JSONPath of(String path) {
59+
if ("$".equals(path)) {
60+
return RootPath.INSTANCE;
61+
}
62+
63+
JSONPathCompiler.CompileResult result = JSONPathCompiler.compile(path);
64+
JSONPathSegment[] segments = result.segments();
65+
boolean definite = result.definite();
66+
67+
// Specialization for common patterns
68+
if (segments.length == 1 && segments[0] instanceof JSONPathSegment.NameSegment ns) {
69+
return new SingleNamePath(ns.name());
70+
}
71+
if (segments.length == 2
72+
&& segments[0] instanceof JSONPathSegment.NameSegment ns1
73+
&& segments[1] instanceof JSONPathSegment.NameSegment ns2) {
74+
return new TwoNamePath(ns1.name(), ns2.name());
75+
}
76+
77+
return new CompiledPath(segments, definite);
78+
}
79+
80+
// ==================== Evaluation ====================
81+
82+
/**
83+
* Evaluate this path on a root object (JSONObject, JSONArray, or Map/List).
84+
* For definite paths: returns a single value or null.
85+
* For indefinite paths: returns a List of matched values.
86+
*/
87+
public abstract Object eval(Object root);
88+
89+
/**
90+
* Evaluate with type conversion.
91+
*/
92+
@SuppressWarnings("unchecked")
93+
public <T> T eval(Object root, Class<T> type) {
94+
Object result = eval(root);
95+
if (result == null) {
96+
return null;
97+
}
98+
if (type.isInstance(result)) {
99+
return type.cast(result);
100+
}
101+
// Numeric conversions
102+
if (result instanceof Number n) {
103+
if (type == int.class || type == Integer.class) {
104+
return (T) Integer.valueOf(n.intValue());
105+
}
106+
if (type == long.class || type == Long.class) {
107+
return (T) Long.valueOf(n.longValue());
108+
}
109+
if (type == double.class || type == Double.class) {
110+
return (T) Double.valueOf(n.doubleValue());
111+
}
112+
if (type == String.class) {
113+
return (T) n.toString();
114+
}
115+
}
116+
if (type == String.class) {
117+
return (T) result.toString();
118+
}
119+
return (T) result;
120+
}
121+
122+
/**
123+
* Whether this path is definite (always returns a single value).
124+
* Definite paths: $.name, $[0], $.a.b
125+
* Indefinite paths: $[*], $..name, $[?...], $[0,1]
126+
*/
127+
public boolean isDefinite() {
128+
return definite;
129+
}
130+
131+
// ==================== Convenience: parse + eval ====================
132+
133+
/**
134+
* Parse JSON string and evaluate this path.
135+
*/
136+
public Object extract(String json) {
137+
Object root = JSON.parse(json);
138+
return eval(root);
139+
}
140+
141+
/**
142+
* Parse JSON string and evaluate this path with type conversion.
143+
*/
144+
public <T> T extract(String json, Class<T> type) {
145+
Object root = JSON.parse(json);
146+
return eval(root, type);
147+
}
148+
149+
/**
150+
* Parse UTF-8 JSON bytes and evaluate this path with type conversion.
151+
*/
152+
public <T> T extract(byte[] jsonBytes, Class<T> type) {
153+
Object root = JSON.parse(new String(jsonBytes, java.nio.charset.StandardCharsets.UTF_8));
154+
return eval(root, type);
155+
}
156+
157+
// ==================== Static convenience ====================
158+
159+
/**
160+
* One-shot: compile path, parse JSON, evaluate, return typed result.
161+
*/
162+
public static <T> T eval(String json, String path, Class<T> type) {
163+
return of(path).extract(json, type);
164+
}
165+
166+
/**
167+
* One-shot: compile path, evaluate on object, return typed result.
168+
*/
169+
public static <T> T eval(Object root, String path, Class<T> type) {
170+
return of(path).eval(root, type);
171+
}
172+
173+
// ==================== Root path: $ ====================
174+
175+
private static final class RootPath extends JSONPath {
176+
static final RootPath INSTANCE = new RootPath();
177+
178+
RootPath() {
179+
super(true);
180+
}
181+
182+
@Override
183+
public Object eval(Object root) {
184+
return root;
185+
}
186+
}
187+
188+
// ==================== Specialized: $.name ====================
189+
190+
private static final class SingleNamePath extends JSONPath {
191+
private final String name;
192+
193+
SingleNamePath(String name) {
194+
super(true);
195+
this.name = name;
196+
}
197+
198+
@Override
199+
public Object eval(Object root) {
200+
if (root instanceof JSONObject obj) {
201+
return obj.get(name);
202+
}
203+
if (root instanceof Map<?, ?> map) {
204+
return map.get(name);
205+
}
206+
return null;
207+
}
208+
}
209+
210+
// ==================== Specialized: $.a.b ====================
211+
212+
private static final class TwoNamePath extends JSONPath {
213+
private final String name1;
214+
private final String name2;
215+
216+
TwoNamePath(String name1, String name2) {
217+
super(true);
218+
this.name1 = name1;
219+
this.name2 = name2;
220+
}
221+
222+
@Override
223+
public Object eval(Object root) {
224+
Object v1;
225+
if (root instanceof JSONObject obj) {
226+
v1 = obj.get(name1);
227+
} else if (root instanceof Map<?, ?> map) {
228+
v1 = map.get(name1);
229+
} else {
230+
return null;
231+
}
232+
if (v1 instanceof JSONObject obj2) {
233+
return obj2.get(name2);
234+
}
235+
if (v1 instanceof Map<?, ?> map2) {
236+
return map2.get(name2);
237+
}
238+
return null;
239+
}
240+
}
241+
242+
// ==================== General compiled path ====================
243+
244+
private static final class CompiledPath extends JSONPath {
245+
private final JSONPathSegment[] segments;
246+
247+
CompiledPath(JSONPathSegment[] segments, boolean definite) {
248+
super(definite);
249+
this.segments = segments;
250+
}
251+
252+
@Override
253+
public Object eval(Object root) {
254+
if (root == null) {
255+
return null;
256+
}
257+
JSONPathContext ctx = new JSONPathContext(root, segments, definite);
258+
if (segments.length > 0) {
259+
ctx.segmentIndex = 0;
260+
segments[0].eval(ctx);
261+
}
262+
return ctx.getResult();
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)