JSON Processing & Binding

Working with JSON in Jakarta EE

← Back to Index

What is JSON Processing in Jakarta EE?

Think of JSON Processing like mail handling:

Jakarta JSON Processing (JSON-P) provides a low-level API for parsing, generating, transforming, and querying JSON data.

Jakarta JSON Binding (JSON-B) provides automatic conversion between Java objects and JSON.

Feature JSON-P (Processing) JSON-B (Binding)
Level Low-level API High-level API
Purpose Parse/generate JSON Object <-> JSON conversion
Control Full manual control Automatic with annotations
Use Case Dynamic JSON, streaming POJOs, DTOs
Package jakarta.json jakarta.json.bind

JSON-P: Processing API

Creating JSON Objects

import jakarta.json.*;

// Create JSON object using builder
JsonObject person = Json.createObjectBuilder()
    .add("name", "John Doe")
    .add("age", 30)
    .add("email", "john@example.com")
    .add("active", true)
    .add("address", Json.createObjectBuilder()
        .add("street", "123 Main St")
        .add("city", "New York")
        .add("zip", "10001"))
    .add("phones", Json.createArrayBuilder()
        .add("555-1234")
        .add("555-5678"))
    .build();

// Convert to string
String jsonString = person.toString();
// {"name":"John Doe","age":30,"email":"john@example.com",...}

Parsing JSON

import jakarta.json.*;
import java.io.StringReader;

String json = """
    {
        "name": "Jane Smith",
        "age": 25,
        "skills": ["Java", "Python", "SQL"]
    }
    """;

// Parse JSON string
try (JsonReader reader = Json.createReader(new StringReader(json))) {
    JsonObject obj = reader.readObject();

    // Access values
    String name = obj.getString("name");          // "Jane Smith"
    int age = obj.getInt("age");                   // 25
    JsonArray skills = obj.getJsonArray("skills"); // ["Java", "Python", "SQL"]

    // Iterate array
    for (JsonValue skill : skills) {
        System.out.println(skill);
    }
}

Streaming API (Large Files)

// Streaming parser - memory efficient for large files
try (JsonParser parser = Json.createParser(new FileReader("large.json"))) {
    while (parser.hasNext()) {
        JsonParser.Event event = parser.next();

        switch (event) {
            case KEY_NAME -> System.out.println("Key: " + parser.getString());
            case VALUE_STRING -> System.out.println("String: " + parser.getString());
            case VALUE_NUMBER -> System.out.println("Number: " + parser.getBigDecimal());
            case VALUE_TRUE -> System.out.println("Boolean: true");
            case VALUE_FALSE -> System.out.println("Boolean: false");
            case VALUE_NULL -> System.out.println("Null");
            case START_OBJECT -> System.out.println("{");
            case END_OBJECT -> System.out.println("}");
            case START_ARRAY -> System.out.println("[");
            case END_ARRAY -> System.out.println("]");
        }
    }
}

// Streaming generator - write large files efficiently
try (JsonGenerator gen = Json.createGenerator(new FileWriter("output.json"))) {
    gen.writeStartArray();
    for (int i = 0; i < 1000000; i++) {
        gen.writeStartObject()
           .write("id", i)
           .write("name", "Item " + i)
           .writeEnd();
    }
    gen.writeEnd();
}

JSON Pointer (Query JSON)

String json = """
    {
        "store": {
            "books": [
                {"title": "Java Guide", "price": 29.99},
                {"title": "Spring Boot", "price": 39.99}
            ]
        }
    }
    """;

JsonObject obj = Json.createReader(new StringReader(json)).readObject();

// Use JSON Pointer to navigate
JsonPointer pointer = Json.createPointer("/store/books/0/title");
JsonValue title = pointer.getValue(obj);
System.out.println(title);  // "Java Guide"

// Get second book's price
JsonPointer pricePointer = Json.createPointer("/store/books/1/price");
JsonNumber price = (JsonNumber) pricePointer.getValue(obj);
System.out.println(price.doubleValue());  // 39.99

JSON Patch (Modify JSON)

JsonObject original = Json.createObjectBuilder()
    .add("name", "John")
    .add("age", 25)
    .build();

// Create patch operations
JsonPatch patch = Json.createPatchBuilder()
    .replace("/name", "John Doe")   // Update name
    .add("/email", "john@mail.com") // Add email
    .remove("/age")                   // Remove age
    .build();

// Apply patch
JsonObject patched = patch.apply(original);
// {"name":"John Doe","email":"john@mail.com"}

JSON-B: Binding API

Basic Serialization/Deserialization

import jakarta.json.bind.*;

// Your POJO
public class Person {
    private String name;
    private int age;
    private String email;

    // Constructors, getters, setters
    public Person() {}
    public Person(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
    // getters and setters...
}

// Create Jsonb instance
Jsonb jsonb = JsonbBuilder.create();

// Object to JSON (Serialization)
Person person = new Person("John", 30, "john@example.com");
String json = jsonb.toJson(person);
// {"age":30,"email":"john@example.com","name":"John"}

// JSON to Object (Deserialization)
String jsonInput = """
    {"name":"Jane","age":25,"email":"jane@example.com"}
    """;
Person parsed = jsonb.fromJson(jsonInput, Person.class);
System.out.println(parsed.getName());  // Jane

Collections and Generics

Jsonb jsonb = JsonbBuilder.create();

// List of objects
List<Person> people = List.of(
    new Person("John", 30, "john@mail.com"),
    new Person("Jane", 25, "jane@mail.com")
);

// Serialize list
String json = jsonb.toJson(people);
// [{"age":30,"email":"john@mail.com","name":"John"},...]

// Deserialize list (need Type for generics)
String jsonList = "[{\"name\":\"John\",\"age\":30},{\"name\":\"Jane\",\"age\":25}]";
List<Person> parsed = jsonb.fromJson(
    jsonList,
    new ArrayList<Person>(){}.getClass().getGenericSuperclass()
);

// Map serialization
Map<String, Integer> scores = Map.of("math", 95, "science", 88);
String mapJson = jsonb.toJson(scores);
// {"math":95,"science":88}

Customizing with Annotations

import jakarta.json.bind.annotation.*;

public class Employee {

    @JsonbProperty("employee_id")  // Custom JSON key name
    private Long id;

    @JsonbProperty("full_name")
    private String name;

    @JsonbTransient  // Exclude from JSON
    private String password;

    @JsonbDateFormat("yyyy-MM-dd")  // Custom date format
    private LocalDate hireDate;

    @JsonbNumberFormat("#,##0.00")  // Custom number format
    private BigDecimal salary;

    @JsonbNillable  // Include null values in JSON
    private String middleName;

    // getters/setters
}

// Result:
// {
//   "employee_id": 123,
//   "full_name": "John Doe",
//   "hireDate": "2024-01-15",
//   "salary": "75,000.00",
//   "middleName": null
// }

Property Order and Naming Strategy

@JsonbPropertyOrder({"id", "name", "email", "phone"})  // Define order
public class Contact {
    private Long id;
    private String name;
    private String email;
    private String phone;
}

// Configure naming strategy
JsonbConfig config = new JsonbConfig()
    .withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES);

Jsonb jsonb = JsonbBuilder.create(config);

public class UserProfile {
    private String firstName;   // becomes "first_name"
    private String lastName;    // becomes "last_name"
    private String emailAddress; // becomes "email_address"
}

Custom Serializer/Deserializer

// Custom serializer
public class MoneySerializer implements JsonbSerializer<Money> {
    @Override
    public void serialize(Money money, JsonGenerator generator,
                          SerializationContext ctx) {
        generator.writeStartObject();
        generator.write("amount", money.getAmount());
        generator.write("currency", money.getCurrency().getCurrencyCode());
        generator.writeEnd();
    }
}

// Custom deserializer
public class MoneyDeserializer implements JsonbDeserializer<Money> {
    @Override
    public Money deserialize(JsonParser parser,
                             DeserializationContext ctx, Type rtType) {
        JsonObject obj = parser.getObject();
        BigDecimal amount = obj.getJsonNumber("amount").bigDecimalValue();
        String currencyCode = obj.getString("currency");
        return new Money(amount, Currency.getInstance(currencyCode));
    }
}

// Use custom serializers
public class Order {
    private Long id;

    @JsonbTypeSerializer(MoneySerializer.class)
    @JsonbTypeDeserializer(MoneyDeserializer.class)
    private Money totalAmount;
}

Jsonb Configuration

JsonbConfig config = new JsonbConfig()
    // Pretty print JSON
    .withFormatting(true)

    // Include null values
    .withNullValues(true)

    // Property naming strategy
    .withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_DASHES)

    // Property visibility (include private fields)
    .withPropertyVisibilityStrategy(new PropertyVisibilityStrategy() {
        @Override
        public boolean isVisible(Field field) { return true; }
        @Override
        public boolean isVisible(Method method) { return true; }
    })

    // Date format
    .withDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)

    // Property ordering
    .withPropertyOrderStrategy(PropertyOrderStrategy.LEXICOGRAPHICAL);

Jsonb jsonb = JsonbBuilder.create(config);

Integration with JAX-RS

@Path("/users")
public class UserResource {

    // JSON-B automatically converts User to/from JSON!

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<User> getAll() {
        return userService.findAll();  // Auto-converted to JSON
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public User create(User user) {  // Auto-parsed from JSON
        return userService.save(user);
    }
}

// Custom Jsonb provider for JAX-RS
@Provider
public class JsonbContextResolver implements ContextResolver<Jsonb> {

    private final Jsonb jsonb;

    public JsonbContextResolver() {
        JsonbConfig config = new JsonbConfig()
            .withFormatting(true)
            .withNullValues(false);
        this.jsonb = JsonbBuilder.create(config);
    }

    @Override
    public Jsonb getContext(Class<?> type) {
        return jsonb;
    }
}

Common Patterns

DTO with Validation

public class CreateUserRequest {

    @NotNull
    @Size(min = 2, max = 50)
    @JsonbProperty("username")
    private String name;

    @NotNull
    @Email
    private String email;

    @JsonbTransient
    private String internalCode;

    @JsonbCreator  // Use this constructor for deserialization
    public CreateUserRequest(
            @JsonbProperty("username") String name,
            @JsonbProperty("email") String email) {
        this.name = name;
        this.email = email;
    }
}

Polymorphic Types

// Base type
public abstract class Notification {
    private String type;
    private String message;
}

// Subtypes
public class EmailNotification extends Notification {
    private String recipient;
    private String subject;
}

public class SmsNotification extends Notification {
    private String phoneNumber;
}

// Custom deserializer for polymorphic types
public class NotificationDeserializer implements JsonbDeserializer<Notification> {
    @Override
    public Notification deserialize(JsonParser parser,
                                     DeserializationContext ctx, Type rtType) {
        JsonObject obj = parser.getObject();
        String type = obj.getString("type");

        return switch (type) {
            case "email" -> ctx.deserialize(EmailNotification.class, parser);
            case "sms" -> ctx.deserialize(SmsNotification.class, parser);
            default -> throw new JsonbException("Unknown type: " + type);
        };
    }
}

Best Practices

DO:

  • Use JSON-B for POJOs - Simpler and cleaner code
  • Use JSON-P for dynamic JSON - When structure is unknown
  • Use streaming for large files - Better memory efficiency
  • Use DTOs - Don't expose entities directly
  • Configure Jsonb once - Reuse the instance
  • Use @JsonbTransient - Hide sensitive fields
  • Handle nulls properly - Configure null handling strategy

DON'T:

  • Don't create Jsonb per request - It's expensive
  • Don't expose passwords/secrets - Use @JsonbTransient
  • Don't ignore date formatting - Use @JsonbDateFormat
  • Don't mix JSON-P and JSON-B unnecessarily - Pick the right tool
  • Don't forget to close readers/parsers - Use try-with-resources

Summary

  • JSON-P: Low-level API for parsing, generating, and querying JSON
  • JSON-B: High-level binding between Java objects and JSON
  • JsonObject/JsonArray: JSON-P immutable data structures
  • Jsonb: Main JSON-B interface for toJson/fromJson
  • @JsonbProperty: Customize JSON field names
  • @JsonbTransient: Exclude fields from JSON
  • @JsonbDateFormat: Custom date formatting
  • JsonbConfig: Configure serialization behavior
  • Streaming API: Memory-efficient for large files
  • JSON Pointer/Patch: Query and modify JSON documents