Java Academy

Master Java Development

Menu

Core Java

Servlets

JDBC

Microservices

Build Tools

APIs

APIs · Lesson 3

JSON Serialization with Jackson

What is JSON Serialization?

Serialization converts Java objects into a format suitable for storage or transmission (like JSON). Deserialization reverses the process, converting JSON back into Java objects. This is fundamental for REST APIs — clients send/receive JSON; your Java code works with objects. Libraries like Jackson and Gson automate this conversion, handling nested objects, arrays, and custom types.

Jackson: The Standard JSON Library

Jackson is the most widely used JSON library in Java. Spring Boot uses Jackson by default. It automatically discovers object properties and converts them to/from JSON. You can customize serialization with annotations like @JsonProperty, @JsonIgnore, and @JsonFormat.

import com.fasterxml.jackson.annotation.*;
import java.util.Date;
import java.util.List;

// User class with Jackson annotations
@JsonIgnoreProperties(ignoreUnknown = true)  // Ignore unknown JSON fields
public class User {
    @JsonProperty("user_id")  // JSON field name differs from Java field name
    private int id;
    
    private String name;
    
    @JsonProperty(required = true)  // Field is required in JSON
    private String email;
    
    @JsonIgnore  // Don't serialize password to JSON
    private String password;
    
    @JsonFormat(pattern = "yyyy-MM-dd")  // Format date in JSON
    private Date createdAt;
    
    @JsonInclude(JsonInclude.Include.NON_NULL)  // Only include non-null fields
    private String bio;
    
    private List<String> tags;  // Lists are automatically serialized
    
    // Constructor, getters, setters...
    public User() {}
    
    public User(int id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
    public String getBio() { return bio; }
    public void setBio(String bio) { this.bio = bio; }
    public List<String> getTags() { return tags; }
    public void setTags(List<String> tags) { this.tags = tags; }
}

// Spring Boot automatically serializes/deserializes with Jackson
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    // GET returns User as JSON
    @GetMapping("/{id}")
    public User getUser(@PathVariable int id) {
        User user = new User(1, "Alice", "alice@example.com");
        user.setCreatedAt(new Date());
        user.setTags(java.util.Arrays.asList("java", "spring"));
        return user;
        // Automatically serialized to JSON:
        // {
        //   "user_id": 1,
        //   "name": "Alice",
        //   "email": "alice@example.com",
        //   "createdAt": "2023-01-15",
        //   "tags": ["java", "spring"]
        // }
        // Note: password and null bio are excluded
    }
    
    // POST accepts JSON and deserializes to User object
    @PostMapping
    public User createUser(@RequestBody User user) {
        // Spring Boot automatically deserializes JSON to User object
        System.out.println("Created user: " + user.getName());
        return user;
    }
}

Custom Serialization with JsonSerialize and JsonDeserialize

For complex types (enums, custom objects), implement custom serializers and deserializers.

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;

// Custom serializer for Price (BigDecimal with 2 decimals)
class PriceSerializer extends JsonSerializer<BigDecimal> {
    @Override
    public void serialize(BigDecimal value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        if (value == null) {
            gen.writeNull();
        } else {
            gen.writeString(String.format("$%.2f", value));  // Format as "$99.99"
        }
    }
}

// Custom deserializer for Price
class PriceDeserializer extends JsonDeserializer<BigDecimal> {
    @Override
    public BigDecimal deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
        String value = p.getValueAsString();
        if (value == null || value.isEmpty()) {
            return null;
        }
        // Remove $ and convert to BigDecimal
        return new BigDecimal(value.replace("$", ""));
    }
}

// Product class with custom price serialization
class Product {
    private int id;
    private String name;
    
    @JsonSerialize(using = PriceSerializer.class)
    @JsonDeserialize(using = PriceDeserializer.class)
    private BigDecimal price;
    
    // Getters, setters...
    public Product() {}
    public Product(int id, String name, BigDecimal price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
    
    public int getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
}

// Usage
import com.fasterxml.jackson.databind.ObjectMapper;

class JsonSerializationExample {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        
        // Java to JSON (serialization)
        Product product = new Product(1, "Laptop", new BigDecimal("999.99"));
        String json = mapper.writeValueAsString(product);
        System.out.println(json);  // {"id":1,"name":"Laptop","price":"$999.99"}
        
        // JSON to Java (deserialization)
        String jsonInput = "{\"id\":2,\"name\":\"Phone\",\"price\":\"$599.99\"}";
        Product product2 = mapper.readValue(jsonInput, Product.class);
        System.out.println(product2.getPrice());  // 599.99 (as BigDecimal)
    }
}

Collections and Nested Objects

Jackson handles lists, maps, and nested objects automatically.

import java.util.List;
import java.util.Map;

class Order {
    private int id;
    private String status;
    private List<OrderItem> items;  // List of nested objects
    private Map<String, String> metadata;  // Key-value pairs
    
    public Order() {}
    
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public List<OrderItem> getItems() { return items; }
    public void setItems(List<OrderItem> items) { this.items = items; }
    public Map<String, String> getMetadata() { return metadata; }
    public void setMetadata(Map<String, String> metadata) { this.metadata = metadata; }
}

class OrderItem {
    private String productId;
    private int quantity;
    private double price;
    
    public OrderItem() {}
    public OrderItem(String productId, int quantity, double price) {
        this.productId = productId;
        this.quantity = quantity;
        this.price = price;
    }
    
    public String getProductId() { return productId; }
    public int getQuantity() { return quantity; }
    public double getPrice() { return price; }
}

// Serialization example
class CollectionsExample {
    public static void main(String[] args) throws Exception {
        // Create order with nested items
        Order order = new Order();
        order.setId(1001);
        order.setStatus("PENDING");
        order.setItems(java.util.Arrays.asList(
            new OrderItem("PROD-001", 2, 49.99),
            new OrderItem("PROD-002", 1, 99.99)
        ));
        order.setMetadata(java.util.Map.of(
            "currency", "USD",
            "shippingMethod", "EXPRESS"
        ));
        
        // Convert to JSON
        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(order);
        System.out.println(json);
        
        // JSON output:
        // {
        //   "id" : 1001,
        //   "status" : "PENDING",
        //   "items" : [ {
        //     "productId" : "PROD-001",
        //     "quantity" : 2,
        //     "price" : 49.99
        //   }, {
        //     "productId" : "PROD-002",
        //     "quantity" : 1,
        //     "price" : 99.99
        //   } ],
        //   "metadata" : {
        //     "currency" : "USD",
        //     "shippingMethod" : "EXPRESS"
        //   }
        // }
    }
}
Jackson AnnotationPurposeExample
@JsonPropertyRename JSON field@JsonProperty("user_id") on field id
@JsonIgnoreExclude from JSON@JsonIgnore on password field
@JsonIgnorePropertiesIgnore multiple fields@JsonIgnoreProperties({"password", "token"})
@JsonFormatFormat dates/numbers@JsonFormat(pattern = "yyyy-MM-dd")
@JsonIncludeInclude/exclude null/empty@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonSerializeCustom serializer@JsonSerialize(using = PriceSerializer.class)
@JsonDeserializeCustom deserializer@JsonDeserialize(using = PriceDeserializer.class)

Common Mistake: Forgetting @JsonProperty when JSON field names differ from Java field names (API uses snake_case, Java uses camelCase). Also, exposing sensitive fields (passwords, tokens) in JSON — use @JsonIgnore on fields that shouldn't serialize. Don't serialize large nested objects unnecessarily; flatten complex structures into DTOs (Data Transfer Objects) for JSON.

Best Practices for JSON Serialization

  • Use @JsonProperty when JSON and Java field names differ
  • Use @JsonIgnore on sensitive fields (passwords, tokens, internal IDs)
  • Flatten complex objects for JSON — create DTOs for API responses
  • Use @JsonFormat for consistent date/time formatting
  • Handle null values with @JsonInclude to reduce payload size
  • Implement custom serializers only for complex types
  • Version your JSON schema if you plan API changes
  • Use generic ObjectMapper configuration in Spring Boot's context for consistency

Real-World Example: User Registration API

A user registration endpoint accepts JSON: POST /api/users with {"name": "Alice", "email": "alice@example.com", "password": "secret"}. Jackson deserializes to User object. The endpoint validates email, hashes password, saves to database. The response excludes password: {"user_id": 1, "name": "Alice", "email": "alice@example.com", "createdAt": "2023-01-15"}. Date is formatted via @JsonFormat. If JSON contains unknown fields, they're ignored (via @JsonIgnoreProperties). Lists and nested objects (like addresses) are automatically serialized. By leveraging Jackson's annotations, the API is clean, secure, and easy to maintain.

Quiz Check

Quick Knowledge Test

Loading quiz questions...