JSON in Quarkus Explained: JSON-B, JSON-P, or Jackson?
A hands-on guide for Java developers to pick the right JSON processor and write cleaner, faster REST APIs.
Every Java developer writes APIs that consume and produce JSON. It’s the common language of the modern web. But not all JSON handling in Java is created equal.
In Quarkus, you have three main flavors:
JSON-B (JSON Binding) – automatic mapping between Java objects and JSON
JSON-P (JSON Processing) – manual control for building, parsing, or streaming JSON
Jackson (ObjectMapper) – the de facto JSON library used across the Java ecosystem
Each has its strengths. Each fits different workloads. This hands-on guide helps you understand when to use which, and how to make them work in Quarkus.
Prerequisites
You’ll need:
Java 17+
Maven 3.8+
A basic understanding of Quarkus REST endpoints
You can also just grab the example from my Github repository!
JSON-B and JSON-P in Quarkus
Let’s start with the official Jakarta EE standards that Quarkus supports out of the box.
Project Setup
Create a new Quarkus project with both extensions:
mvn io.quarkus:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=json-demo \
-Dextensions="rest-jackson,jsonb,jsonp"
cd json-demoThis adds:
jsonbfor high-level object bindingjsonpfor low-level streaming and manipulationrest-jacksonso you can compare with Jackson later
Understanding JSON-B: High-Level Object Mapping
JSON-B (JSON Binding) is the official Jakarta EE standard for converting Java objects to JSON and back. It’s annotation-driven, minimal, and ideal for REST APIs.
Create a model class:
package com.example.model;
import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbDateFormat;
import java.time.LocalDate;
public class Product {
private Long id;
private String name;
private Double price;
@JsonbProperty("stock_quantity")
private Integer stockQuantity;
@JsonbDateFormat("yyyy-MM-dd")
private LocalDate releaseDate;
public Product() {
}
public Product(Long id, String name, Double price, Integer stockQuantity, LocalDate releaseDate) {
this.id = id;
this.name = name;
this.price = price;
this.stockQuantity = stockQuantity;
this.releaseDate = releaseDate;
}
// Getters and setters omitted for brevity
}
Create a JSON-B service:
package com.example.service;
import java.time.LocalDate;
import java.util.List;
import com.example.model.Product;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
@ApplicationScoped
public class JsonbService {
private final Jsonb jsonb;
public JsonbService() {
JsonbConfig config = new JsonbConfig().withFormatting(true);
this.jsonb = JsonbBuilder.create(config);
}
public String serializeProduct(Product product) {
return jsonb.toJson(product);
}
public Product deserializeProduct(String json) {
return jsonb.fromJson(json, Product.class);
}
public Product sampleProduct() {
return new Product(1L, "Laptop", 999.99, 50, LocalDate.of(2024, 1, 15));
}
public String serializeList(List<Product> products) {
return jsonb.toJson(products);
}
}JSON-B automatically maps Java fields to JSON keys. The annotations let you customize names, formats, and inclusion rules.
Understanding JSON-P: Low-Level Processing
JSON-P (JSON Processing) gives you manual control over JSON structure. It’s useful for dynamic data, large payloads, or stream-based operations.
Create a JSON-P service:
package com.example.service;
import java.io.StringReader;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
@ApplicationScoped
public class JsonpService {
public String buildProductJson(Long id, String name, Double price, Integer stock) {
JsonObject product = Json.createObjectBuilder()
.add("id", id)
.add("name", name)
.add("price", BigDecimal.valueOf(price))
.add("stock_quantity", stock)
.add("metadata", Json.createObjectBuilder()
.add("featured", true)
.add("tags", Json.createArrayBuilder()
.add("electronics").add("computers")))
.build();
return product.toString();
}
public Map<String, Object> parseProductJson(String jsonString) {
JsonReader reader = Json.createReader(new StringReader(jsonString));
JsonObject obj = reader.readObject();
Map<String, Object> result = new HashMap<>();
result.put("id", obj.getJsonNumber("id").longValue());
result.put("name", obj.getString("name"));
result.put("price", obj.getJsonNumber("price").doubleValue());
return result;
}
public String applyDiscount(String json, double percent) {
JsonObject product = Json.createReader(new StringReader(json)).readObject();
double newPrice = product.getJsonNumber("price").doubleValue() * (1 - percent / 100);
JsonObject updated = Json.createObjectBuilder(product)
.add("price", newPrice)
.add("discount_applied", percent)
.build();
return updated.toString();
}
}
JSON-P gives you total control over the JSON structure — but you have to manage it all manually.
REST Endpoints to Compare Both
Create a resource class:
package com.example.resource;
import java.util.Map;
import com.example.service.JsonbService;
import com.example.service.JsonpService;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/api/json”)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class JsonDemoResource {
@Inject
JsonbService jsonbService;
@Inject
JsonpService jsonpService;
@GET
@Path(”/jsonb/product”)
public Response jsonbProduct() {
return Response.ok(jsonbService.serializeProduct(jsonbService.sampleProduct())).build();
}
@GET
@Path(”/jsonp/product”)
public Response jsonpProduct() {
return Response.ok(jsonpService.buildProductJson(1L, “Tablet”, 599.99, 75)).build();
}
@POST
@Path(”/jsonp/discount”)
public Response applyDiscount(String json, @QueryParam(”percent”) double percent) {
return Response.ok(jsonpService.applyDiscount(json, percent)).build();
}
@GET
@Path(”/compare”)
public Response compare() {
return Response.ok(Map.of(
“jsonb”, “Object mapping for REST APIs”,
“jsonp”, “Fine-grained control for dynamic or large JSON”)).build();
}
}Test It
Start the application:
mvn quarkus:devThen call:
curl http://localhost:8080/api/json/jsonb/product
curl http://localhost:8080/api/json/jsonp/product
curl -X POST "http://localhost:8080/api/json/jsonp/discount?percent=20" \
-H "Content-Type: application/json" \
-d '{"id":1,"name":"Laptop","price":999.99}'
JSON-B vs JSON-P: When to Use Which
Use JSON-B when:
You build REST endpoints that map objects cleanly to JSON.
Your schema is stable.
You want readable, minimal code.
Use JSON-P when:
You stream or process very large JSON files.
You need to build or modify JSON dynamically.
Your data doesn’t fit into Java classes easily.
You can even mix them — JSON-B for mapping and JSON-P for transformation.
Enter Jackson — The De Facto Standard
While JSON-B and JSON-P are official Jakarta standards, Jackson is the library most Java developers already know. It’s fast, feature-rich, and used by Spring Boot and many other frameworks.
Model with Jackson Annotations
package com.example.model;
import java.time.LocalDateTime;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ProductJ {
private Long id;
private String name;
@JsonProperty(”unit_price”)
private Double price;
@JsonProperty(”stock_quantity”)
private Integer stock;
@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”)
private LocalDateTime createdAt = LocalDateTime.now();
@JsonIgnore
private String internalCode;
public ProductJ() {
}
public ProductJ(Long id, String name, Double price, Integer stock) {
this.id = id;
this.name = name;
this.price = price;
this.stock = stock;
this.internalCode = “INTERNAL-” + id;
}
// Getters and setters omitted for brevity
}Jackson offers a vast ecosystem of annotations for naming, ignoring, formatting, inclusion, and polymorphism.
Inject and Use ObjectMapper
package com.example.service;
import java.util.Map;
import com.example.model.ProductJ;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@ApplicationScoped
public class JacksonService {
@Inject
ObjectMapper mapper;
public String toJson(ProductJ product) throws Exception {
return mapper.writeValueAsString(product);
}
public ProductJ fromJson(String json) throws Exception {
return mapper.readValue(json, ProductJ.class);
}
public Map<String, Object> toMap(ProductJ product) {
return mapper.convertValue(product, new TypeReference<Map<String, Object>>() {
});
}
}Quarkus injects a preconfigured ObjectMapper. You can also customize it globally.
What is ObjectMapper?
ObjectMapper is the main class in Jackson that handles:
Converting Java objects to JSON (serialization)
Converting JSON to Java objects (deserialization)
Configuring how JSON processing works
Reading/writing JSON in various formats
Think of it as your Swiss Army knife for JSON in Jackson.
Quarkus creates and manages the ObjectMapper for you. But you often want to customize how JSON serialization/deserialization works. Instead of creating your own ObjectMapper, you customize the one Quarkus provides. This ensures your settings apply everywhere Jackson is used in your app.
package com.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.quarkus.jackson.ObjectMapperCustomizer;
import jakarta.inject.Singleton;
@Singleton
public class JacksonConfig implements ObjectMapperCustomizer {
@Override
public void customize(ObjectMapper objectMapper) {
// Pretty print by default
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
// Don’t fail on unknown properties
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Write dates as ISO-8601 strings, not timestamps
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// Support Java 8 date/time types
objectMapper.registerModule(new JavaTimeModule());
// Include only non-null values
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
}The most important configurations in practice:
FAIL_ON_UNKNOWN_PROPERTIES = false- Essential for real-world APIs that evolveJavaTimeModule- Required for modern Java date/time typesWRITE_DATES_AS_TIMESTAMPS = false- Makes dates human-readableNON_NULLinclusion - Reduces payload size significantly
The beauty of this pattern is that Quarkus handles all the plumbing - you just implement the interface, and your configuration is automatically applied everywhere.
REST + Jackson
package com.example.resource;
import java.util.List;
import java.util.Map;
import com.example.model.ProductJ;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(”/api/products”)
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {
@GET
@Path(”{id}”)
public ProductJ getProduct(@PathParam(”id”) Long id) {
return new ProductJ(id, “Laptop”, 999.99, 50);
}
@POST
public Map<String, Object> create(ProductJ product) {
return Map.of(
“message”, “Product created”,
“name”, product.getName(),
“price”, product.getPrice());
}
@GET
public List<ProductJ> list() {
return List.of(
new ProductJ(1L, “Laptop”, 999.99, 50),
new ProductJ(2L, “Mouse”, 29.99, 200));
}
}No manual conversion — Jackson handles it all.
Choosing the Right Flavor
Jackson (ObjectMapper) = More features, faster, industry standard
JSON-B + JSON-P = Jakarta EE standard, simpler, enterprise-friendly
Most modern Quarkus projects use Jackson because it’s more powerful and has better performance, but JSON-B is perfectly fine if you prefer standards-based APIs or work in environments that mandate Jakarta EE compliance.
Don’t mix JSON-B and Jackson in the same project—they conflict.
Run and Explore
mvn quarkus:devVisit the Dev UI at http://localhost:8080/q/dev to inspect endpoints and configuration.
Choose the flavor that matches your use case — Quarkus supports them all beautifully.




Thanks for sharing Markus. I encourage developers to prefer JSON-B / JSON-P to Jackson. While Jackson made perfect sense as long as there wasn't any standard JSON spec, now that an official standard is available since several years, there is no any more reason to use an "industrial standard", i.e. a non standard solution like Jackson.
If it's true that the curent standard isn't fast enough and doesn't provide enough features, then these are all good reasons to improve it, instead of using an allegedly better but non standard library.