Wednesday, June 5, 2024

Map Subset of JSON via Jackson

Map Subset of JSON via Jackson

1. Introduction


JavaScript Object Notation (JSON) is a text-based data format and widely used in the APIs for exchanging data between a client and server. The Jackson library from FasterXML is the most popular library for serializing Java objects to JSON and vice-versa. Sometimes, the JSON data returned from the server is a complex data structure as it considers all clients’ requirements. However, a client may only need a subset of the JSON data. This is similar to creating a database view based on tables. In this example, I will demonstrate how to map a subset of JSON using Jackson libraries.

2. Project Setup


In this step, I will set up a maven project with Jackson libraries to read three JSON files and map a subset of JSON into a Java POJO.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.zheng.demo</groupId>
    <artifactId>jackson-subset</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
 
        <!--
        https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.1</version>
        </dependency>
 
        <!--
        https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.17.1</version>
        </dependency>
 
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Here is the screenshot of the project setup.

Map Subset of JSON via Jackson

Figure 1. Project Setup

Note: There are two sets of Customer and Order POJOs which map to the desired subset of the JSON fields. The ones under org.zheng.demo.data package do not have the root element configured but the ones under org.zheng.demo.type package have the root element configured.

3. JSON Files


In this step, I will create three JSON files that will be used at step 4 and 5.

  • customer.json – this JSON contains a customer and its two orders.
  • customerWithWrapRoot.json – this JSON contains the default root from Jackson – the simple class name.
  • customWithJsonType.json – this JSON contains a customized root from Jackson with @JsonTypeName annotation.

3.1 Customer JSON File

The customer.json file contains a customer and its two orders.

customer.json

{
    "custTag" : "major",
    "email" : "test@test.com",
    "id" : 30,
    "name" : "Zheng",
    "orders" : [ {
      "productName" : "test",
      "quantity" : 10,
      "ignoreOrder":"should not be found in POJO"
    } ,
     {
      "productName" : "test",
      "quantity" : 1,
      "ignoreOrder":"should not be found in POJO"
    } ],
    "phone":"636-123-2345",
    "balance":1234.56,
    "rewardPoint":100
  }

Note: for this example, the client only wants to map the highlighted fields: custTag, email, id, name, and order‘s productName and quantity.

3.2 Custom with Wrapp_Root JSON File

The customerWithWrapRoot.json contains the root element with its simple class name – Customer.

customerWithWrapRoot.json

{
    "Customer": {
        "custTag": "major",
        "email": "test@test.com",
        "id": 30,
        "name": "Zheng",
        "unknowCustom": "not showing in this project",
        "orders": [
            {
                "productName": "test",
                "unknowOrder": "not showing in this project",
                "quantity": 10
            },
            {
                "productName": "test",
                "unknowOrder": "not showing in this project",
                "quantity": 10
            }
        ],
        "phone":"636-123-2345",
        "balance":1234.56,
        "rewardPoint":100
    }
}

◉ Line 2: this JSON contains the default wrap_root: Customer.

3.3 Customer with Customized Root JSON File

The customerWithJsonType.json contains the customized root element name – customer.

customerWithJsonType.json

{
    "customer": {
        "custTag": "major",
        "email": "test@test.com",
        "id": 30,
        "name": "Zheng",
        "orders": [
            {
                "order": {
                    "productName": "test",
                    "unknowOrder": "not showing in this project",
                    "quantity": 10
                }
            },
            {
                "order": {
                    "productName": "test",
                    "unknowOrder": "not showing in this project",
                    "quantity": 10
                }
            }
        ],
        "phone":"636-123-2345",
        "balance":1234.56,
        "rewardPoint":100
    }
}

◉ Line 2: this JSON contains the “customer” root node.
◉ Line 9, 16: the orders node contains the “order” root node.

4. Map Subset to Java Object


In this step, I will create two Java data objects annotated with @JsonIgnoreProperties(ignoreUnknown =true) so they can be mapped to a subset of the JSON file created at step 3.1.

4.1 Customer Object

In this step, I will create a simple Customer class which annotates with @JsonIgnoreProperties(ignoreUnknown = true). This class contains five fields from a total of eight fields from Customer.json file.

Customer.java

package org.zheng.demo.data;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
@JsonIgnoreProperties(ignoreUnknown = true)
public class Customer implements Serializable {
 
    private static final long serialVersionUID = 5963349342478710542L;
 
    private String custTag;
 
    private String email;
 
    private int id;
 
    private String name;
 
    private List<Order> orders;
 
    public Customer() {
        super();
    }
 
    public Customer(String name, int id) {
        super();
        this.id = id;
        this.name = name;
    }
 
    public void addOrder(Order order) {
        if (this.orders == null) {
            this.orders = new ArrayList<>();
        }
        this.orders.add(order);
    }
 
    public String getCustTag() {
        return custTag;
    }
 
    public String getEmail() {
        return email;
    }
 
    public int getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public List<Order> getOrders() {
        return orders;
    }
 
    public void setCustTag(String custTag) {
        this.custTag = custTag;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }
 
}

◉ Note: only custTag, email, id, name, and orders are mapped.

4.2 Order Object

In this step, I will create a simple Order.java which maps two fields from a total of three fields from Customer.json‘s orders node.

Order.java

package org.zheng.demo.data;
 
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
@JsonIgnoreProperties(ignoreUnknown = true)
public class Order {
 
    private String productName;
 
    private int quantity;
 
    public Order() {
        super();
    }
 
    public Order(int quantity, String name) {
        super();
        this.quantity = quantity;
        this.productName = name;
    }
 
    public String getProductName() {
        return productName;
    }
 
    public int getQuantity() {
        return quantity;
    }
 
    public void setProductName(String name) {
        this.productName = name;
    }
 
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
 
}

Note: only productName and quantity are mapped.

4.3 Customer & Order with Root

In this step, I will create another set of Customer and Order classes under the org.zheng.demo.type package.

The Customer class has a similar data structure as defined at step 4.1 except with the @JsonTypeName annotation and has an extra field: rewardPoint.

Customer.java

package org.zheng.demo.type;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeName("customer") 
@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=Id.NAME)
public class Customer implements Serializable {
 
    private static final long serialVersionUID = 5963349342478710542L;
 
    private String custTag;
 
    private String email;
 
    private int id;
 
    private String name;
 
    private List<Order> orders;
     
    private int rewardPoint;
 
    public Customer() {
        super();
    }
 
    public Customer(String name, int id) {
        super();
        this.id = id;
        this.name = name;
    }
 
    public void addOrder(Order order) {
        if (this.orders == null) {
            this.orders = new ArrayList<>();
        }
        this.orders.add(order);
    }
 
    public String getCustTag() {
        return custTag;
    }
 
    public String getEmail() {
        return email;
    }
 
    public int getId() {
        return id;
    }
 
    public String getName() {
        return name;
    }
 
    public List<Order> getOrders() {
        return orders;
    }
 
    public void setCustTag(String custTag) {
        this.custTag = custTag;
    }
 
    public void setEmail(String email) {
        this.email = email;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }
 
    public int getRewardPoint() {
        return rewardPoint;
    }
 
    public void setRewardPoint(int rewardPoint) {
        this.rewardPoint = rewardPoint;
    }
 
}

The Order.java class is same as the class defined at step 4.2 except the @JsonTypeName annotation.

Order.java

package org.zheng.demo.type;
 
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeName;
 
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeName("order") 
@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=Id.NAME)
public class Order {
 
    private String productName;
 
    private int quantity;
 
    public Order() {
        super();
    }
 
    public Order(int quantity, String name) {
        super();
        this.quantity = quantity;
        this.productName = name;
    }
 
    public String getProductName() {
        return productName;
    }
 
    public int getQuantity() {
        return quantity;
    }
 
    public void setProductName(String name) {
        this.productName = name;
    }
 
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
 
}

5. Demo How to Map a Subset of Json Using Jackson


5.1 Read via JsonNode

In this step, I will create a Junit test class Jackson_Node_Test.java which utilizes the JsonNode tree model to parse the JSON and extract the subset of fields dynamically.

Jackson_Node_Test.java

package org.zheng.demo;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Customer;
import org.zheng.demo.data.Order;
 
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
 
class Jackson_Node_Test {
 
    @Test
    void test_readJsonFromFile_via_readTree() {
        ObjectMapper ob = new ObjectMapper();
        File jsonFile = new File("src/test/resources/customer.json");
        try {
            JsonNode jsonNodes = ob.readTree(jsonFile);
 
            String name = jsonNodes.get("name").asText();
            int id = jsonNodes.get("id").asInt();
            Customer cust = new Customer(name, id);
 
            cust.setCustTag(jsonNodes.get("custTag").asText());
            cust.setEmail(jsonNodes.get("email").asText());
 
            List<Order> orders = new ArrayList<>();
            cust.setOrders(orders);
 
            JsonNode ordersNode = jsonNodes.get("orders");
            if (ordersNode.isArray()) {
                for (JsonNode orderNode : ordersNode) {
                    JsonNode nameNode = orderNode.get("productName");
                    JsonNode quantityNode = orderNode.get("quantity");
                    orders.add(new Order(quantityNode.asInt(), nameNode.asText()));
                }
            }
 
            assertEquals("major", cust.getCustTag());
            assertEquals("test@test.com", cust.getEmail());
            assertEquals("Zheng", cust.getName());
            assertEquals(30, cust.getId());
            assertEquals(2, cust.getOrders().size());
            assertEquals("test", cust.getOrders().get(0).getProductName());
 
            System.out.println(ob.writerWithDefaultPrettyPrinter().writeValueAsString(cust));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
}

◉ Line 22: create a file object from the src/test/resources/customer.json.
◉ Line 24: use the objectMapper.readTree to obtain the JsonNode.
◉ Line 26,27,28: parse the customer’s id and name data from JsonNode and create a customer object.
◉ Line 31, 32: parse the custTag and email data from JsonNode.
◉ Line 36-41: parse the orders from JsonNode.

Execute this Junit test – test_readJsonFromFile_via_readTree and capture the output.

test_readJsonFromFile_via_readTree output

{
  "custTag" : "major",
  "email" : "test@test.com",
  "id" : 30,
  "name" : "Zheng",
  "orders" : [ {
    "productName" : "test",
    "quantity" : 10
  }, {
    "productName" : "test",
    "quantity" : 1
  } ]
}

Note: as you see, the mapped customer object contains a subset of the original customer.json file.

5.2 Ignore Unknown Properties

In this step, I will create a Junit test class JacksonTest which uses @JsonIgnoreProperties(ignoreUnknown = true) and ObjectMapper to map a subset of JSON fields based on the Java POJO. There are two tests:

◉ test_readJsonFromFile_via – read the JSON from the customer.json file and map a subset into org.zheng.demo.data.Customer.
◉ test_readFile_withCustomizedRoot – read the JSON from the customerWithJsonType file and map a subset into org.zheng.demo.type.Customer.

JacksonTest.java

package org.zheng.demo;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
 
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Customer;
 
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
class JacksonTest {
 
    private ObjectMapper ob = new ObjectMapper();
 
    @Test
    void test_readFile_withCustomizedRoot() {
        File jsonFile = new File("src/test/resources/customerWithJsonType.json");
 
        try {
            org.zheng.demo.type.Customer readCust = ob.readValue(jsonFile, org.zheng.demo.type.Customer.class);
 
            assertEquals("major", readCust.getCustTag());
            assertEquals("test@test.com", readCust.getEmail());
            assertEquals("Zheng", readCust.getName());
            assertEquals(30, readCust.getId());
            assertEquals(2, readCust.getOrders().size());
            assertEquals("test", readCust.getOrders().get(0).getProductName());
             
            String jsonStr = ob.writerWithDefaultPrettyPrinter().writeValueAsString(readCust);
            System.out.println(jsonStr);
 
 
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
 
    @Test
    void test_readJsonFromFile_via() throws StreamReadException, DatabindException, IOException {
 
        try (InputStream inputStream = JacksonTest.class.getResourceAsStream("/customer.json")) {
            if (inputStream == null) {
                System.out.println("File not found");
                return;
            }
 
            Customer cust = ob.readValue(inputStream, Customer.class);
 
            assertEquals("major", cust.getCustTag());
            assertEquals("test@test.com", cust.getEmail());
            assertEquals("Zheng", cust.getName());
            assertEquals(30, cust.getId());
            assertEquals(2, cust.getOrders().size());
            assertEquals("test", cust.getOrders().get(0).getProductName());
 
            String jsonStr = ob.writerWithDefaultPrettyPrinter().writeValueAsString(cust);
            System.out.println(jsonStr);
 
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
 
}

◉ Line 22: read the JSON file from "src/test/resources/customerWithJsonType.json".
◉ Line 25: use the org.zheng.demo.type.Customer class when utilizing objectMapper.readValue method.
◉ Line 47: read the JSON file from "/customer.json"
◉ Line 53: use the org.zheng.demo.data.Customer class when calling objectMapper.readValue method.
◉ Line 34, 62: print out the subset mapped objects.
◉ Execute this Junit test and capture the output.

test_readJsonFromFile_via output

{
  "custTag" : "major",
  "email" : "test@test.com",
  "id" : 30,
  "name" : "Zheng",
  "orders" : [ {
    "productName" : "test",
    "quantity" : 10
  }, {
    "productName" : "test",
    "quantity" : 1
  } ]
}
{
  "customer" : {
    "custTag" : "major",
    "email" : "test@test.com",
    "id" : 30,
    "name" : "Zheng",
    "orders" : [ {
      "order" : {
        "productName" : "test",
        "quantity" : 10
      }
    }, {
      "order" : {
        "productName" : "test",
        "quantity" : 10
      }
    } ],
    "rewardPoint" : 100
  }
}

Note: as you see, only a subset of data are mapped. Also if the JSON has a customized root, then org.zheng.demo.type.Customer is used.

5.3 Handle the Wrap_oot

In this step, I will create a Junit Test class Jackson_wrapRootTest. It has two test methods that map a subset based on the default root.

◉ test_readFile_withWrapRoot – read a JSON string from customerWithWrapRoot.json file and map it with DeserializationFeature.UNWRAP_ROOT_VALUE.
◉ test_write_read_withRoot– verify the root is added to the JSON when SerializationFeature.WRAP_ROOT_VALUE is enabled.

Jackson_WrapRootTest.java

package org.zheng.demo;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
 
import org.junit.jupiter.api.Test;
import org.zheng.demo.data.Customer;
import org.zheng.demo.data.Order;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
 
class Jackson_WrapRootTest {
 
    private ObjectMapper ob = new ObjectMapper();
 
    @Test
    void test_readFile_withWrapRoot() {
        File jsonFile = new File("src/test/resources/customerWithWrapRoot.json");
 
        try {
            Customer readCust = ob.readerFor(Customer.class).with(DeserializationFeature.UNWRAP_ROOT_VALUE)
                    .readValue(jsonFile);
 
            assertEquals("major", readCust.getCustTag());
            assertEquals("test@test.com", readCust.getEmail());
            assertEquals("Zheng", readCust.getName());
            assertEquals(30, readCust.getId());
            assertEquals(2, readCust.getOrders().size());
            assertEquals("test", readCust.getOrders().get(0).getProductName());
 
        } catch (IOException e) {
            e.printStackTrace();
        }
 
    }
 
    @Test
    void test_write_read_withRoot() {
        Customer cust = new Customer("Zheng", 30);
        Order order = new Order();
        cust.setEmail("test@test.com");
        List orders = new ArrayList();
        order.setProductName("test");
        order.setQuantity(10);
        orders.add(order);
        orders.add(order);
        cust.setOrders(orders);
 
        try {
            String jsonString = ob.enable(SerializationFeature.WRAP_ROOT_VALUE).writeValueAsString(cust);
            System.out.println("Serialized JSON: " + jsonString);
 
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
 
    }
}

◉ Line 25: read JSON from customerWithWrapRoot.json.
◉ Line 28, 57: set DeserializationFeature.UNWRAP_ROOT_VALUE when reading and SerializationFeature.WRAP_ROOT_VALUE when writing.
Execute this Junit test and capture the output.

Jackson_wrapRootTest output

Serialized JSON: {"Customer":{"custTag":null,"email":"test@test.com","id":30,"name":"Zheng","orders":[{"productName":"test","quantity":10},{"productName":"test","quantity":10}]}}

Source: javacodegeeks.com

Related Posts

0 comments:

Post a Comment