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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Tackle Complex JSON in Spring Boot with a Single JsonPath Annotation

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootCustom AnnotationJsonPathJSON ParsingArgument Resolver
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.