Tackle Complex JSON in Spring Boot with a Single JsonPath Annotation
This article explains how to simplify handling deeply nested JSON responses in Spring Boot by creating a custom @JsonPath annotation that leverages JsonPath expressions, allowing one‑line extraction, filtering, and modification of JSON data directly in controller method parameters.
Environment: Spring Boot 3.5.0
1. Introduction
In Spring Boot development, third‑party APIs often return deeply nested JSON whose fields may change. Manually parsing each level produces verbose code and high maintenance cost. JsonPath can locate data efficiently, but writing JsonPath.parse(json).read(...) each time is not elegant.
This article shows how to create a custom annotation so that JsonPath extraction works like injecting a configuration value, enabling one‑line JSON extraction and mapping.
2. Practical Example
2.1 Quick Start
Dependency:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>JsonPath expressions use the same root symbol $ as XPath. They can be written with dot notation: $.store.book[0].title or bracket notation: $['store']['book'][0]['title'] Operators, functions and filter operators are illustrated in the following images:
Sample JSON data used in the examples:
{
"store": {
"book": [
{
"category": "e-book",
"author": "Pack",
"title": "Spring全家桶实战案例",
"price": 66.6
},
{
"category": "e-book",
"author": "Pack_xg",
"title": "Spring Boot3实战案例300讲",
"price": 70
},
...
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}Examples:
// Read all authors
List<String> authors = JsonPath.read(json, "$.store.book[*].author");
// Output: [Pack, Pack_xg]
// Read specific index
String author0 = JsonPath.read(json, "$.store.book[0].author");
String author1 = JsonPath.read(json, "$.store.book[1].author");
// Output: author0: Pack, author1: Pack_xg
// Filter books with price > 68
List<Map<String, Object>> books = JsonPath.parse(json)
.read("$.store.book[?(@.price > 68)]");
// Output: [{category=e-book, author=Pack_xg, title=Spring Boot3实战案例300讲, price=70}]
// Predicate filter for e‑book and price < 70
Filter cheapFictionFilter = filter(
where("category").is("e-book").and("price").lt(70D));
List<Map<String, Object>> books = parse(json)
.read("$.store.book[?]", cheapFictionFilter);
// Output: [{category=e-book, author=Pack, title=Spring全家桶实战案例, price=66.6}]
// Modify a value
String newJson = JsonPath.parse(json)
.set("$['store']['book'][0]['author']", "XXXOOO")
.jsonString();
// Output shows author changed to "XXXOOO"When a leaf node is missing, JsonPath throws PathNotFoundException. Adding Option.DEFAULT_PATH_LEAF_TO_NULL makes it return null instead, as demonstrated with a small JSON array.
2.2 Custom Parameter Parsing
Define an annotation that holds a JsonPath expression:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface JsonPath {
String value() default "";
}Implement a HandlerMethodArgumentResolver that reads the request body, extracts the JsonPath from the annotation, and converts the result to the target parameter type:
public class JsonPathHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final String ROOT = "$";
private final ObjectMapper objectMapper;
public JsonPathHandlerMethodArgumentResolver(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(JsonPath.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
JsonPath annotation = parameter.getParameterAnnotation(JsonPath.class);
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String json = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
if (!StringUtils.hasLength(json)) {
return null;
}
Class<?> paramType = parameter.getParameterType();
String path = annotation.value();
if (!StringUtils.hasLength(path)) {
return objectMapper.readValue(json, paramType);
}
return com.jayway.jsonpath.JsonPath.parse(json)
.read(ROOT + "." + path, paramType);
}
}Register the resolver in a configuration class:
@Configuration
public class WebConfig implements WebMvcConfigurer, InitializingBean {
private final ObjectMapper objectMapper;
public WebConfig(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void afterPropertiesSet() throws Exception {
com.jayway.jsonpath.Configuration.setDefaults(
new com.jayway.jsonpath.Configuration.Defaults() {
public Set<Option> options() { return EnumSet.noneOf(Option.class); }
public MappingProvider mappingProvider() { return new JacksonMappingProvider(); }
public JsonProvider jsonProvider() { return new JacksonJsonProvider(); }
});
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new JsonPathHandlerMethodArgumentResolver(objectMapper));
}
}2.3 Test Controller
Expose endpoints that use the annotation:
@RestController
@RequestMapping("/jsonpath")
public class JsonPathController {
@PostMapping
public Object read(@JsonPath("store.book[*]") List<Book> data) {
return data;
}
// other variants:
// @JsonPath("store.book[0]") Book data
// @JsonPath("store.book[?(@.price > 68)]") List<Book> data
// @JsonPath("store.book[*].author") List<String> data
}The four endpoints return the results shown in the following screenshots:
These results demonstrate that a single custom annotation can replace repetitive JsonPath parsing code and make controller methods concise.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
