A thin, fluent wrapper over Jackson for building, reading, and mapping JSON with the minimum amount of code.
Three everyday tasks, side by side:
Jackson:
List<String> names = new ArrayList<>();
JsonNode arr = mapper.readTree(json).get("employees");
if (arr != null && arr.isArray()) {
for (JsonNode emp : arr) {
if (emp.has("name")) {
names.add(emp.get("name").asText());
}
}
}yupzip-json:
List<String> names = Json.parse(json)
.stream("employees")
.map(e -> e.string("name"))
.toList();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);
String json = mapper.writeValueAsString(root);yupzip-json:
String json = Json.create()
.put("id", 1)
.put("name", "John")
.put("address", Json.create()
.put("city", "Sydney")
.put("state", "NSW"))
.toString();Jackson:
String state = mapper.readTree(json)
.path("customer").path("address").path("state").asText();yupzip-json:
String state = Json.parse(json).string("customer.address.state");- No annotations on your POJOs.
Jsonis aMap-backed value, not a code-generated class. - Fluent everywhere. Build, read, and map JSON in single expressions.
- Caller-driven typing. Pick the accessor that matches the value (
string(),integer(),object(),array()). The library doesn't second-guess you. - Spring-friendly. Works as
@RequestBody JsonandResponseEntity<Json>out of the box.
This library requires JDK 17+
(yupzip-json with JDK 1.8 support is 1.8.5)
<dependency>
<groupId>com.yupzip.json</groupId>
<artifactId>yupzip-json</artifactId>
<version>4.3.0</version>
</dependency>implementation group: 'com.yupzip.json', name: 'yupzip-json', version: '4.3.0'Fluent JSON object creation:
Json person = Json.create()
.put("id", 1)
.put("name", "John Citizen")
.add("gender", personEntity.getGender()) //adds property only if value is not null
.put("weight", 90.1)
.put("verified", true)
.put("contactNumbers", List.of("0400000000", "0400000001"))
.put("address", Json.create()
.put("addressLine", "100 George Street")
.put("postCode", "2000")
.put("state", "NSW")
.put("country", "Australia"))
.put("dob", "1990-01-01");For small objects, Json.of(...) is a concise shorthand. Pass alternating string keys and values:
Json user = Json.of("id", 10, "username", "test.user", "verified", true);
Json person = Json.of("id", 1, "address", Json.of("city", "Sydney", "state", "NSW"));Odd argument counts and non-string keys fail fast with IllegalArgumentException.
Json person = Json.create();
int id = person.integer("id");
String name = person.string("name");
String gender = person.stringOr("gender", "unknown"); //returns property value or default value if null
Double weight = person.decimal("weight");
Long orderId = order.longInt("orderId"); // longInt chosen as getter short name as 'long' being a keyword
BigDecimal price = order.bigDecimal("price");
List<BigDecimal> amounts = order.bigDecimals("amounts");
UUID requestId = order.uuid("requestId");
List<String> contactNumbers = person.strings("contactNumbers");
Json address = person.object("address");
Date dob = person.date("dob", "yyyy-MM-dd");
LocalDate today = person.localDate("today", "yyyy-MM-dd");
// default values and exception behavior
int qty = order.integerOr("qty", 1); // default if missing
String id = order.stringOrThrow("id"); // PropertyRequiredException if missing
String id = order.stringOrThrow("id", new MyApiError()); // custom exception
Company company = loadCompany();
List<String> employeeNames = Json.parse(company)
.stream("employees")
.map(employee -> employee.string("fullName"))
.collect(Collectors.toList());Three variants per type. Choose by what you want on absent/null values:
| Variant | Behavior |
|---|---|
| `string(key)` | returns value, or `null` if missing |
| `stringOr(key, default)` | returns default if missing |
| `stringOrThrow(key)` | throws `PropertyRequiredException` if missing |
| `stringOrThrow(key, ex)` | throws your exception if missing |
The same family exists for integer, longInt, decimal, bigDecimal, uuid, bool, object, date, localDate.
Any read accessor accepts a dot-separated path to walk nested objects and arrays. [N] indexes into a list. Wrap a key containing a literal . in backticks (`) to opt out of path parsing:
String state = order.string("customer.address.state"); // nested object
BigDecimal firstPrice = order.bigDecimal("items[0].price"); // array index
String firstSku = order.string("items[0].sku");
String dottedKeyValue = order.string("`weird.key.with.dots`"); // literal key
// Works with all variants and presence checks
String postcode = order.stringOr("customer.address.postCode", "0000");
String postcode2 = order.stringOrThrow("customer.address.postCode");
boolean hasStateKey = order.hasKey("customer.address.state");Paths apply to reads only. put, add, append, and remove always operate on top-level keys of the current object.
Helper methods:
// boolean helpers: isTrue, isFalse, anyTrue, anyFalse allTrue, allFalse, valueEquals
user.isTrue("active"); // null-safe
user.isFalse("suspended");
user.allTrue("active", "verified");
user.anyFalse("active", "verified");
user.valueEquals("status", "OPEN");
// find() - deep lookup
String street = response.find("streetLine", String.class); // recurses nested objects/arrays
// static helper methods
Json.isValid(payload);
byte[] bytes = ...;
Json json = Json.parse(bytes);
Person person = Json.parseAs(jsonString, Person.class);
String personJson = Json.asString(person);
Optional<Json> maybe = Json.from(maybeNullObject);Fluent mapping of JSON properties:
Json response = Json.create(); //response payload
Person person = new Person();
Address address = new Address();
response.map("name", person::setName) // generic mapping of property value (type is defined by consumer)
.integer("id", person::setId) // or mapping explicit types
.decimal("weight", person::setWeight)
.bool("verified", person::setVerified)
.strings("contactNumbers", person::setContactNumbers)
.integers("numbers", person::setNumbers)
.decimals("scores", person::setScores)
.object("address", addressJson -> addressJson // or mapping child json object
.map("addressLine", address::setAddressLine)
.map("postCode", address::setPostCode)
.map("state", address::setState)
.map("country", address::setCountry));Parsing JSON string:
String personString = """
{
"id": 1,
"name": "John Citizen"
}
""";
Json person = Json.parse(personString);Converting from/to Java POJOs:
Person person = new Person(1, "John Citizen");
Json json = Json.parse(person);Person person = Json.create()
.put("id", 1)
.put("name", "John Citizen")
.convertTo(Person.class);person.remove("id");
person.remove("dob", "address", "employer");
person.remove(List.of("dob", "address", "employer"));Works as @RequestBody because Json carries @JsonAnySetter / @JsonAnyGetter:
@PutMapping("/v1/customers")
public void createCustomer(@RequestBody Json request) {
customerService.createCustomer(request);
}RestTemplate response body:
public List<Product> getProducts(String url) {
ResponseEntity<Json> 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();
}yupzip-json sits in a specific niche. Here's how it lines up against the obvious alternatives.
When you own the data model and the schema is stable, POJOs give you type-checked code end to end, IDE support, and lossless round-tripping. For domain objects in your service, this is the right tool. The library is not trying to replace this workflow.
When you query nested values out of an existing payload using rich expressions (filters, wildcards, recursive descent, slicing), JsonPath's query syntax is more expressive than yupzip's simple dot-and-bracket paths. If your code mostly extracts data from documents you've already received, JsonPath is the right tool.
When JSON is the working format through a layer:
- Forwarding webhooks where you touch a few fields and pass them on.
- Integration code where the upstream schema is third-party and unstable.
- Building dynamic JSON responses where the shape depends on the request.
- Adapter code that takes JSON in, mutates a few keys, and returns JSON out.
For the fraction of code where the program does not deeply inspect the JSON, the typed-accessor and fluent-builder style cuts more boilerplate than POJOs save.
On Spring Boot 4.0+, add the spring-boot-starter-yupzip-json dependency and yupzip-json will share Spring's JsonMapper bean automatically. Every Json.parse(...), convertTo(...), and toString() call uses the same mapper as the rest of your application, configured by spring.jackson.*. Zero code changes required.
Maven:
<dependency>
<groupId>com.yupzip.json</groupId>
<artifactId>spring-boot-starter-yupzip-json</artifactId>
<version>1.1.0</version>
</dependency>Gradle:
implementation 'com.yupzip.json:spring-boot-starter-yupzip-json:1.1.0'For Spring Boot 3.x or manual setup, see the starter README.
Configuration via application.properties:
jackson.property-naming-strategy=UPPER_CAMEL_CASE
jackson.deserialization.fail-on-unknown-properties=false
jackson.serialization.fail-on-empty-beans=false
jackson.default-property-inclusion=ALWAYS
jackson.visibility.field=ANY
jackson.visibility.getter=NONE
jackson.visibility.is-getter=NONE
jackson.visibility.setter=NONE
jackson.disabled-features=WRITE_DATES_AS_TIMESTAMPS,FAIL_ON_EMPTY_BEANSdisabled-features / enabled-features accept any comma-separated values from SerializationFeature, DeserializationFeature, or MapperFeature.
JSON properties (keys/values) are stored in a java.util.Map Map<String, Object> properties.
This map is a HashMap by default, however this can be changed to LinkedHashMap if required via property:
yupzip.json.map-type=LINKED_HASH_MAPThis project is licensed under Apache License Version 2.0 - LICENSE.md