Spring Boot 4 just shipped with native Jackson 3 support. Good moment to revisit how much Java we write when we work with JSON.
If you've built a Spring service, you've probably written variations of this dozens of times:
JsonNode root = mapper.readTree(json).get("employees");
List<String> names = new ArrayList<>();
if (root != null && root.isArray()) {
for (JsonNode emp : root) {
if (emp.has("name")) {
names.add(emp.get("name").asText());
}
}
}
It's idiomatic. It's safe. It's also nine lines to get a list of names from an array.
I maintain a small library called yupzip-json that's been running in production at my work for years. This week I shipped 4.1 and a Spring Boot 4 starter — so a good moment to write about it.
What it is
yupzip-json is a thin, fluent wrapper over Jackson. It doesn't replace Jackson — it sits on top, so you keep all the customisation you've already configured (spring.jackson.*, modules, mixins). What it gives you is a Json type that's:
- A
Map-backed value with typed accessors — no POJOs required - Fluent for building, reading, and mapping
- Path-aware for nested reads (
customer.address.state,items[0].price)
Same example, rewritten:
List<String> names = Json.parse(json)
.stream("employees")
.map(e -> e.string("name"))
.toList();
Same Jackson underneath. Less Java on top.
A couple more side-by-sides
Building a JSON object.
Jackson:
ObjectNode root = mapper.createObjectNode();
root.put("id", 1);
root.put("name", "John");
ObjectNode address = mapper.createObjectNode();
address.put("city", "Sydney");
address.put("state", "NSW");
root.set("address", address);
yupzip-json:
Json employee = Json.create()
.put("id", 1)
.put("name", "John")
.put("address", Json.create()
.put("city", "Sydney")
.put("state", "NSW"));
Reading a nested value:
Jackson:
String state = mapper.readTree(json)
.path("customer")
.path("address")
.path("state")
.asText();
yupzip-json:
String state = Json.parse(json).string("customer.address.state");
One method, dot-path, returns null if anything's missing.
Why no POJOs?
For plenty of real-world cases you don't want a POJO:
- Third-party APIs where the schema is unstable enough that you'd rather not commit to types
- Dynamic responses where fields appear or disappear by context
- Adapter code that takes JSON in, tweaks a few keys, returns JSON out
When that's the case, defining a POJO is overhead with no benefit. yupzip-json gives you typed access without the type definitions.
The library has an opinionated design: the caller picks the accessor that matches the value. Call string() on a nested object and you get its toString form — that's by design. The library doesn't second-guess your intent or auto-coerce types. You write object() for objects, string() for strings, and you don't pay for runtime defensiveness you didn't ask for.
Spring REST
Spring controller
Same fluent code inside an endpoint. Because Json carries @JsonAnySetter and @JsonAnyGetter, Spring's HttpMessageConverter accepts it as a request body.
Reading request body
@PostMapping("/customers")
public void create(@RequestBody Json request) {
Customer customer = new Customer();
request.map("firstName", customer::setFirstName)
.map("lastName", customer::setLastName)
customerService.saveCustomer(customer);
}
Building response
@GetMapping("/customers/{id}")
public Json get(@PathVariable Long id) {
Customer customer = customerService.find(id);
return Json.create()
.put("id", customer.getId())
.put("name", customer.getName())
.put("address", Json.create()
.put("city", customer.getCity())
.put("state", customer.getState()));
}
RestTemplate
public List<Product> getProducts(String url) {
var responseEntity = restTemplate.getForEntity(url, Json.class);
Json response = Objects.requireNonNull(responseEntity.getBody());
return response.stream("data")
.map(item -> Product.of(item.integer("id"))
.withName(item.string("name"))
.withPrice(item.decimal("price")))
.toList();
}
Spring Boot 4 zero-config
On Spring Boot 4, you add the starter and that's it:
implementation 'com.yupzip.json:spring-boot-starter-yupzip-json:1.1.0'
The starter picks up Spring's JsonMapper bean — configured via spring.jackson.* as normal — and wires it into yupzip. Every Json.parse(...), convertTo(...), toString() call uses the same mapper as the rest of your application. No naming-strategy surprises between your @RestController responses and your Json instances.
On Spring Boot 3.x the starter still works — you just declare a @Bean JsonMapper (Jackson 3) yourself, since SB 3 only auto-provides Jackson 2's ObjectMapper. Details in the starter README.
<dependency>
<groupId>com.yupzip.json</groupId>
<artifactId>yupzip-json</artifactId>
<version>4.1.0</version>
</dependency>
implementation 'com.yupzip.json:yupzip-json:4.1.0'
It's been in production at my work for years but mostly unknown outside of it. If you try it, I'd genuinely like to know what you think — wins, friction, what's missing.
GitHub: yupzip/yupzip-json
Starter: yupzip/spring-boot-starter-yupzip-json