Showing posts with label JSON. Show all posts
Showing posts with label JSON. Show all posts

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

Monday, November 20, 2023

Using JSON Relational Duality Views with Micronaut Framework

Using JSON Relational Duality Views with Micronaut Framework

Oracle JSON Relational Duality delivers a capability that provides the benefits of both relational tables and JSON documents, without the trade-offs of either approach. The new feature in Oracle Database 23c that enables this capability is referred to as a JSON Relational Duality View.

Using Duality Views, data is still stored in relational tables in a highly efficient normalized format but is accessed by applications in the form of JSON documents. Developers can thus think in terms of JSON documents for data access while using highly efficient relational data storage, without having to compromise simplicity. In addition, Duality Views hide all the complexities of database level concurrency control from the developer, providing document-level serializability.

In this blog post, we provide an example of using the Micronaut Framework to create and interact with a JSON Relational Duality View.

The source for the example is available on github, and we'll look at particular snippets to demonstrate how to use Micronaut Data with Duality Views.

1. The Example Application


Our example is a simple relational database application that represents a student course schedule. A student has a course with a name, a time, a location, and a teacher. A simple example like this uses data stored in multiple normalized relational tables: a student table, a teacher table, a course table, and a table mapping students to their courses. But it is not always straightforward for developers, even in a simple example like this, to build the course schedule for one student, say, "Jill". The developer has to retrieve data from all four tables to assemble Jill's schedule. What the developer really wants is to build Jill's schedule using a single database operation.

What if we could use JSON documents to build this application? That would really simplify database access. JSON is very popular as an access and interchange format because it is so simple.

For example, the course schedule could be represented in a JSON document as a simple hierarchy of key-value pairs. So, Jill's schedule could be as simple as a single JSON document, providing details of each of her courses (name, time, location, and teacher).

However, JSON has limitations as a storage format because of data duplication and consistency. Even in the simple example of student schedules, the course and teacher information is stored redundantly in each student's course schedule document. Duplicate data is inefficient to store, expensive to update, and difficult to keep consistent.

JSON Document Relational Duality Views combine the benefits of the Relational and the Document approach.

A duality view declares the recipe for assembling normalized rows into a JSON document using SQL or GraphQL syntax. The structure of the view mirrors the structure of your desired JSON document. Then you can select from the duality view using SQL, and return Jill's course schedule as a JSON document. You can also update the JSON document that represents Jill's course schedule and the duality view updates the underlying database tables.

1.1. Application Configuration

The application is configured in src/main/resources/application.yml, as follows:

micronaut: 
  application:
    name: OracleJsonDemo
  server:
    thread-selection: io
datasources: # <2>
  default:
    schema-generate: none
    packages: org.com.example.entity
    dialect: oracle
test-resources: # <1>
  containers:
    oracle:
      image-name: gvenzl/oracle-free:latest-faststart
      startup-timeout: 360s
      db-name: test
flyway: # <3>
  datasources:
    default:
      enabled: true
      baseline-version: 0
      baseline-on-migrate: true

In addition to the name of the application, the configuration file contains three properties that are required by this example application:

1. Test resources: an oracle database container image.
2. Datasources: to indicate the database dialect, and the package(s) to be used.
3. Flyway: to automate the creation of the database schema, including the tables and relational duality view. Micronaut integration with Flyway automatically triggers schema migration before the Micronaut application starts.

1.2 Application Schema

Flyway reads SQL commands in the resources/db/migration/ directory, runs them if necessary, and verifies that the configured data source is consistent with them. The example application contains two files:

  • src/main/resources/db/migration/V1__schema.sql: this creates the COURSE, STUDENT, TEACHER, and STUDENT_COURSE tables, and adds foreign key constraints between them.
  • src/main/resources/db/migration/V2__view.sql: this creates the STUDENT_SCHEDULE relational duality view.

Let's take a closer look at the second of those two files:

CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW "STUDENT_SCHEDULE" AS -- <1>
SELECT JSON{
        'studentId': s."ID", -- <2>
        'student': s."NAME" WITH UPDATE, -- <3>
        'averageGrade': s."AVERAGE_GRADE" WITH UPDATE,
        'schedule': [SELECT JSON{'id': sc."ID", -- <4>
                                 'course': (SELECT JSON{'courseId': c."ID", -- <5>
                                                       'teacher': (SELECT JSON{'teacherId': t."ID", -- <6>
                                                                                'teacher': t."NAME"}
                                                                    FROM "TEACHER" t WITH UPDATE WHERE c."TEACHER_ID" = t."ID"),
                                                       'room': c."ROOM",
                                                       'time': c."TIME",
                                                       'name': c."NAME" WITH UPDATE}
                                           FROM "COURSE" c WITH UPDATE WHERE sc."COURSE_ID" = c."ID")}
                      FROM "STUDENT_COURSE" sc WITH INSERT UPDATE DELETE WHERE s."ID" = sc."STUDENT_ID"]}
FROM "STUDENT" s WITH UPDATE INSERT DELETE;

  1. Create a duality view named STUDENT_SCHEDULE. It maps to the StudentScheduleView class described below.
  2. The ID column of the STUDENT table.
  3. The NAME column of the STUDENT table, which can be updated.
  4. The value of the schedule key is the result of a SELECT SQL operation.
  5. The value of the course key is the result of a SELECT SQL operation. It maps to the CourseView class described below.
  6. The value of the teacher key is the result of a SELECT SQL operation. It maps to the TeacherView class described below.

1.3. Application Domain

The example application consists of domain classes (in the package com.example.micronaut.entity) corresponding to the database tables (implemented as Java Record types):

  • Course
  • Student
  • Teacher
  • StudentCourse

It also includes the following view classes (in the com.example.micronaut.entity.view package) corresponding to JSON documents (also implemented as Java Record types):

  • CourseView: provides a JSON document view of a row in the COURSE table. It maps to the value of the course key described above.
  • StudentView: provides a JSON document view of a row in the STUDENT table.
  • TeacherView: provides a JSON document view of a row in the TEACHER table. It maps to the value of the teacher key described above.
  • StudentScheduleView: maps to the STUDENT_SCHEDULE view declared above.

Within the same package, the class Metadata is used to control concurrency.

Finally, the application provides a record named CreateStudentDto to represent the data transfer object to create a new student. The implementation is in the com.example.micronaut.dto package.

1.4. Database Operations

The application requires interfaces to define operations to access the database. Micronaut Data implements these interfaces at compile time. In the com.example.micronaut.repository package there is a repository interface corresponding to each table, as follows:

  • CourseRepository
  • StudentRepository
  • TeacherRepository
  • StudentCourseRepository

There is an additional interface in the com.example.micronaut.repository.view package named StudentViewRepository, which provides a repository for instances of StudentView.

1.5. Application Controller

The application controller, StudentController (defined in src/main/java/com/example/micronaut/controller/StudentController.java), provides the API to the application, as follows:

@Controller("/students") // <1>
public final class StudentController {

    private final CourseRepository courseRepository;
    private final StudentRepository studentRepository;
    private final StudentCourseRepository studentCourseRepository;
    private final StudentViewRepository studentViewRepository;

    public StudentController(CourseRepository courseRepository, StudentRepository studentRepository, StudentCourseRepository studentCourseRepository, StudentViewRepository studentViewRepository) { // <2>
        this.courseRepository = courseRepository;
        this.studentRepository = studentRepository;
        this.studentCourseRepository = studentCourseRepository;
        this.studentViewRepository = studentViewRepository;
    }

    @Get("/") // <3>
    public Iterable<StudentView> findAll() {
        return studentViewRepository.findAll();
    }

    @Get("/student/{student}") // <4>
    public Optional<StudentView> findByStudent(@NonNull String student) {
        return studentViewRepository.findByStudent(student);
    }

    @Get("/{id}") // <5>
    public Optional<StudentView> findById(Long id) {
        return studentViewRepository.findById(id);
    }

    @Put("/{id}/average_grade/{averageGrade}") // <6>
    public Optional<StudentView> updateAverageGrade(Long id, @NonNull Double averageGrade) {
        //Use a duality view operation to update a student's average grade
        return studentViewRepository.findById(id).flatMap(studentView -> {
            studentViewRepository.updateAverageGrade(id, averageGrade);
            return studentViewRepository.findById(id);
        });
    }

    @Put("/{id}/student/{student}") // <7>
    public Optional<StudentView> updateStudent(Long id, @NonNull String student) {
        //Use a duality view operation to update a student's name
        return studentViewRepository.findById(id).flatMap(studentView -> {
            studentViewRepository.updateStudentByStudentId(id, student);
            return studentViewRepository.findById(id);
        });
    }

    @Post("/") // <8>
    @Status(HttpStatus.CREATED) 
    public Optional<StudentView> create(@NonNull @Body CreateStudentDto createDto) {
      // Use a relational operation to insert a new row in the STUDENT table
      Student student = studentRepository.save(new Student(createDto.student(), createDto.averageGrade()));
      // For each of the courses in createDto parameter, insert a row in the STUDENT_COURSE table
      courseRepository.findByNameIn(createDto.courses()).stream()
          .forEach(course -> studentCourseRepository.save(new StudentCourse(student, course)));
      return studentViewRepository.findByStudent(student.name());
    }

    @Delete("/{id}") // <9>
    @Status(HttpStatus.NO_CONTENT)
    void delete(Long id) {
        //Use a duality view operation to delete a student
        studentViewRepository.deleteById(id);
    }

    @Get("/max_average_grade") // <10>
    Optional<Double> findMaxAverageGrade() {
        return studentViewRepository.findMaxAverageGrade();
    }
}

  1. The class is defined as a controller with the @Controller annotation mapped to the path /students.
  2. Use constructor injection to inject beans of types CourseRepository, StudentRepository, StudentCourseRepository, and StudentViewRepository.
  3. The @Get annotation maps a GET request to /students, which attempts to retrieve a list of students, represented as instances of StudentView.
  4. The @Get annotation maps a GET request to /students/student/{name}, which attempts to retrieve a student, represented as an instance of StudentView. This illustrates the use of a URL path variable (student).
  5. The @Get annotation maps a GET request to /students/{id}, which attempts to retrieve a student, represented as an instance of StudentView.
  6. The @Put annotation maps a PUT request to /students/{id}/average_grade/{averageGrade}, which attempts to update a student's average grade.
  7. The @Put annotation maps a PUT request to /students/{id}/student/{student}, which attempts to update a student's name.
  8. The @Post annotation maps a POST request to /students/, which attempts to create a new student. (The method uses relational operations to insert rows into the STUDENT and STUDENT_COURSE tables.)
  9. The @Delete annotation maps a DELETE request to /students/{id}, which attempts to delete a student.
  10. The @Get annotation maps a GET request to /students/max_average_grade, which returns the maximum average grade for all students.

1.6. Main Class

Like all Micronaut applications, the entry point for the example application is the the Application class in the package com.example.micronaut. It uses constructor injection to inject beans of type CourseRepository, StudentRepository, TeacherRepository, and StudentCourseRepository. It includes a main() method (which starts the application) and an init() method which populates the database tables using relational operations.

2. Run the Application


Run the application using the following command (it will start the application on port 8080):

Copy code snippet
Copied to ClipboardError: Could not CopyCopied to ClipboardError: Could not Copy
./gradlew run
./gradlew run

Wait until the application has started and created the database schema. Your output should look something like:

Jul 31, 2023 4:55:27 PM org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory create
INFO: Creating Schema History table "TEST"."flyway_schema_history" ...
Jul 31, 2023 4:55:28 PM org.flywaydb.core.internal.command.DbMigrate migrateGroup
INFO: Current version of schema "TEST": << Empty Schema >>
Jul 31, 2023 4:55:28 PM org.flywaydb.core.internal.command.DbMigrate doMigrateGroup
INFO: Migrating schema "TEST" to version "1 - schema"
Jul 31, 2023 4:55:31 PM org.flywaydb.core.internal.command.DbMigrate doMigrateGroup
INFO: Migrating schema "TEST" to version "2 - view"
Jul 31, 2023 4:55:31 PM org.flywaydb.core.internal.command.DbMigrate logSummary
INFO: Successfully applied 2 migrations to schema "TEST", now at version v2 (execution time 00:00.772s)
16:55:34.164 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 123859ms. Server Running: http://localhost:8080

3. Test the Application


Test the application by using curl to call the API, implemented by the StudentController class. (We recommend using jq to improve the readability of the JSON output.)

1. List all the students and their schedules by running the following command.

curl --silent http://localhost:8080/students | jq '.'

You should see output similar to the following.

[
  {
    "studentId": 1,
    "student": "Denis",
    "averageGrade": 8.5,
    "schedule": [
      {
        "id": 1,
        "course": {
          "courseId": 1,
          "name": "Math",
          "teacher": {
            "teacherId": 2,
            "teacher": "Mr. Graeme"
          },
          "room": "A101",
          "time": "10:00:00"
        }
      },
      {
        "id": 4,
        "course": {
          "courseId": 3,
          "name": "History",
          "teacher": {
            "teacherId": 1,
            "teacher": "Ms. Olya"
          },
          "room": "A103",
          "time": "12:00:00"
        }
      }
    ],
    "_metadata": {
      "etag": "FF95AEFCF102491B75E75DB54EF1385A",
      "asof": "000000000021C4BB"
    }
  },
...
]

2. Retrieve a schedule by student name.

curl --silent http://localhost:8080/students/student/Jill | jq '.'

3.Retrieve a schedule by student id. The output should look similar to above for the student named "Devjani".

curl --silent http://localhost:8080/students/3 | jq '.'

4. Create a new student with courses (and view that student's schedule). The output should be familiar.

curl --silent \
    -d '{"student":"Sandro", "averageGrade":8.7, "courses": ["Math", "English"]}' \
    -H "Content-Type: application/json" \
    -X POST http://localhost:8080/students | jq '.'

5. Update a student's average grade (by student id).

curl --silent -X PUT http://localhost:8080/students/1/average_grade/9.8| jq '.'

6. Retrieve the maximum average grade.

curl http://localhost:8080/students/max_average_grade

7. Update a student's name (by student id), for example, to correct a typo.

curl --silent -X PUT http://localhost:8080/students/1/student/Dennis | jq '.'

8. Delete a student (by student id) and retrieve the new maximum average grade (to confirm deletion).

curl -X DELETE http://localhost:8080/students/1
curl http://localhost:8080/students/max_average_grade

Discussion

We can see from the tests above how the view classes (in the com.example.micronaut.entity.view package) provide the output. Let's look at Jill's schedule in detail. The output is produced by the findByStudent() method; it returns an instance of StudentView, which is rendered as a String. You should see output similar to the following, which we have annotated. You can see that the structure of the output mirrors the structure of the STUDENT_SCHEDULE relational duality view created in src/main/resources/db/migration/V2__view.sql. If you have time, take a look at the view classes to see how they implement the structure below.

{ // Start of StudentView
  "studentId": 2,
  "student": "Jill",
  "averageGrade": 7.2,
  "schedule": [
    { // Start of StudentScheduleView
      "id": 2,
      "course": { // Start of CourseView
        "courseId": 1,
        "name": "Math",
        "teacher": { // Start of TeacherView
          "teacherId": 2,
          "teacher": "Mr. Graeme"
        }, // End of TeacherView
        "room": "A101",
        "time": "10:00:00"
      } // End of CourseView
    }, //End of StudentScheduleView
    { // Start of StudentScheduleView
      "id": 5,
      "course": { //Start of CourseView
        "courseId": 2,
        "name": "English",
        "teacher": { // Start of TeacherView
          "teacherId": 3,
          "teacher": "Prof. Yevhen"
        }, // End of TeacherView
        "room": "A102",
        "time": "11:00:00"
      } // End of CourseView
    } // End of StudentScheduleView
  ],
  "_metadata": {
    "etag": "5C51516688936720969FE3DBBAA3CEF5",
    "asof": "000000000021F3D4"
  }
} // End of StudentView

Source: oracle.com

Monday, December 12, 2022

Efficient JSON serialization with Jackson and Java


When you’re building distributed systems in Java, the problem of serialization naturally arises. Briefly, serialization is the act of creating a representation of an object to store or transmit it and then reconstruct the same object in a different context.

Oracle Java Certification, Oracle Java Career, Java Jobs, Java Prep, Java Tutorial and Materials, Java Learning, Oralce Java JSON

That context could be

◉ Needing the same object in the same JVM but at a different time
◉ Needing the same object in a different JVM, which might be on a different machine
◉ Needing the same object in a non-JVM application

The last of these possibilities deserves a bit more thought. On the one hand, working with a non-JVM application opens the possibility of sharing objects with the whole world of network-connected applications. On the other hand, it can be hard to understand what is meant by “same object” when the object is reconstituted in something that isn’t a JVM.

Java has a built-in serialization mechanism that is likely to have been partially responsible for some of Java’s early success. However, the design of this mechanism is today viewed as seriously deficient, as Brian Goetz wrote in this 2019 post, “Towards better serialization.” While the JDK team has researched ways to rehabilitate (or maybe just remove) the inbuilt platform-level serialization in future versions of Java, developers’ needs to serialize and transport objects have not gone away.

In modern Java applications, serialization is usually performed using an external library as an explicitly application-level concern, with the result being a document encoded in a widely deployed serialization format. The serialization document, of course, can be stored, retrieved, shared, and archived. A preferred format was, once upon a time, XML; in recent years, JavaScript Object Notation (JSON) has become a more popular choice.

Why you should serialize in JSON


JSON is an attractive choice for a serialization format. The following are some of the reasons:

◉ JSON is extremely simple.
◉ JSON is human-readable.
◉ JSON libraries exist for nearly every programming language.

These benefits are counterbalanced by some negatives; the biggest is that a document serialized by JSON can be quite large, which can contribute to poor performance for larger messages. Note, however, that XML can create even larger documents.

Also, JSON and Java evolved from very different programming traditions. JSON provides for a very restricted set of possible value types.

◉ Boolean
◉ Number
◉ String
◉ Array
◉ Object
◉ null

Of these, JSON’s Boolean, String, and null map fairly closely to Java’s conception of boolean, String, and null, respectively. Number is essentially Java’s double with some corner cases. Array can be thought of as essentially a Java List or ArrayList with some differences.

(The inability of JSON and JavaScript to express an integer type that corresponds to int or long turns out to cause its own headaches for JavaScript developers.)

The JSON Object, on the other hand, is problematic for Java developers due to a fundamental difference in the way that JavaScript approaches object-oriented programming (OOP) compared to how Java approaches OOP.

A class comparison. JavaScript does not natively support classes. Instead, it simulates class-like inheritance using functions. The recently added class keyword in JavaScript is effectively syntactic sugar; it offers a convenient declarative form for JavaScript classes, but the JavaScript class does not have the same semantics as Java classes.

Java’s approach to OOP treats class files as metadata to describe the fields and methods present on objects of the corresponding type. This description is completely prescriptive, as all objects of a given class type have exactly the same set of methods and fields.

Therefore, Java does not permit you to dynamically add a field or a method to a single object at runtime. If you want to define a subset of objects that have extra fields or methods, you must declare a subclass. JavaScript has no such restrictions: Methods or fields can be freely added to individual objects at any time.

JavaScript’s dynamic free-form nature is at the heart of the differences between the object models of the two languages: JavaScript’s conception of an object is most similar to that of a Map<String, Object> in Java. It is important to recognize that the type of the JavaScript value here is Object and not ?, because JavaScript objects are heterogeneous, meaning their values can have a substructure and can be of Array or Object types in their own right.

To help you navigate these difficulties, and automatically bridge the gap between Java’s static view and JavaScript’s dynamic view of the world, several libraries and projects have been developed. Their primary purpose is to handle the serialization and deserialization of Java objects to and from documents in a JSON format. In the rest of this article, I’ll focus on one of the most popular choices: Jackson.

Introducing Jackson


Jackson was first formally released in May 2009 and aims to satisfy the three major constraints of being fast, correct, and lightweight. Jackson is a mature and stable library that provides multiple different approaches to working with JSON, including using annotations for some simple use cases.

Jackson provides three core modules.

◉ Streaming (jackson-core) defines a low-level streaming API and includes JSON-specific implementations.
◉ Annotations (jackson-annotations) contains standard Jackson annotations.
◉ Databind (jackson-databind) implements data binding and object serialization.

Adding the databind module to a project also adds the streaming and annotation modules as transitive dependencies.

The examples to follow will focus on these core modules; there are also many extensions and tools for working with Jackson, which won’t be covered here.

Example 1: Simple serialization


The following code fragment from a university’s information system has a very simple class for the people in the system:

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public int getAge() {
        return age;
    }
}

Jackson can be used to automatically serialize this class to JSON so that it can, for example, be sent over the network to another service that may or may not be implemented in Java and that can receive JSON-formatted data.

You can set up this serialization with a very simple bit of code, as follows:

var grant = new Person("Grant", "Hughes", 19);

var mapper = new ObjectMapper();
try {
    var json = mapper.writeValueAsString(grant);
    System.out.println(json);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

This code produces the following simple output:

{"firstName":"Grant","lastName":"Hughes","age":19}

The key to this code is the Jackson ObjectMapper class. This class has two minor wrinkles that you should know about.

◉ Jackson 2 supports Java 7 as the baseline version.
◉ ObjectMapper expects getter (and setter, for deserialization) methods for all fields.

The first point is not immediately relevant (it will be in the next example, which is why I’m calling it out now), but the second could represent a design constraint for designing the classes, because you may not want to have getter methods that obey the JavaBeans conventions.

It is possible to control various aspects of the serialization (or deserialization) process by enabling specific features on the ObjectMapper. For example, you could activate the indentation feature, as follows:

var mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

Then the output will instead look somewhat more human-readable, but without affecting its functionality.

{
  "firstName" : "Grant",
  "lastName" : "Hughes",
  "age" : 19
}

Example 2: Using Java 17 language features


This example introduces some Java 17 language features to help with the data modelling by making Person an abstract base class that prescribes its possible subclasses—in other words, a sealed class. I’ll also change from using an explicit age, and instead I’ll use a LocalDate to represent the person’s date of birth so the student’s age can be programmatically calculated by the application when needed.

public abstract sealed class Person permits Staff, Student {
    private final String firstName;
    private final String lastName;
    private final LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public LocalDate getDob() {
        return dob;
    }

    // ...
}

The Person class has two direct subclasses, Staff and Student.

public final class Student extends Person {
    private final LocalDate graduation;

    private Student(String firstName, String lastName, LocalDate dob, LocalDate graduation) {
        super(firstName, lastName, dob);
        this.graduation = graduation;
    }

    // Simple factory method
    public static Student of(String firstName, String lastName, LocalDate dob, LocalDate graduation) {
        return new Student(firstName, lastName, dob, graduation);
    }

    public LocalDate getGraduation() {
        return graduation;
    }

    // equals, hashcode, and toString elided
}

You can serialize with driver code, which will be slightly more complex.

var dob = LocalDate.of(2002, Month.MARCH, 17);
var graduation = LocalDate.of(2023, Month.JUNE, 5);
var grant = Student.of("Grant", "Hughes", dob, graduation);

var mapper = new ObjectMapper()
                .enable(SerializationFeature.INDENT_OUTPUT)
                .registerModule(new JavaTimeModule());

try {
    var json = mapper.writeValueAsString(grant);
    System.out.println(json);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

The code above produces the following output:

{
  "firstName" : "Grant",
  "lastName" : "Hughes",
  "dob" : [ 2002, 3, 17 ],
  "graduation" : [ 2023, 6, 5 ]
}

As mentioned earlier, Jackson still requires only Java 7 as a minimum version, and it’s geared around that version. This means that if your objects depend on Java 8 APIs directly (such as classes from java.time), the serialization must use a specific Java 8 module (JavaTimeModule). This class must be registered when the mapper is created—it is not available by default.

To handle that requirement, you will also need to add a couple of extra dependencies to the Jackson libraries’ default. Here they are for a Gradle build script (written in Kotlin).

implementation("com.fasterxml.jackson.core:jackson-databind:2.13.1")
implementation("com.fasterxml.jackson.module:jackson-modules-java8:2.13.1")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.1")

Example 3: Using annotations


The first two examples made it look easy to use Jackson: You created an ObjectMapper object, and the code was automatically able to understand the structure of the Student object and render it into JSON.

However, in practice things are rarely this simple. Here are some real-world situations that can quickly arise when you use Jackson in actual production applications.

In some circumstances, you need to give Jackson a little help. For example, you might want or need to remap the field names from your class into different names in the serialized JSON. Fortunately, this is easy to do with annotations.

public class Person {
    @JsonProperty("first_name")
    private final String firstName;
    @JsonProperty("last_name")
    private final String lastName;
    private final int age;
    private final List<string> degrees;

    public Person(String firstName, String lastName, int age, List<string> degrees) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.degrees = degrees;
    }

    // ... getters for all fields
}

Your code will produce some output that looks like the following:

{
  "age" : 19,
  "degrees" : [ "BA Maths", "PhD" ],
  "first_name" : "Grant",
  "last_name" : "Hughes"
}

Note that the field names are now different from the JSON keys and that a List of Java strings is being represented as a JSON array. This is the first usage of annotations in Jackson that you are seeing—but it won’t be the last.

Example 4: Deserialization with JSON


Everything so far has involved serialization of Java objects to JSON. What happens when you want to go the other way? Fortunately, the ObjectMapper provides a reading API as well as a writing API. Here is how the reading API works; this example also uses Java 17 text blocks, by the way.

var json = """
            {
                "firstName" : "Grant",
                "lastName" : "Hughes",
                "age" : 19
            }""";

var mapper = new ObjectMapper();
try {
    var grant = mapper.readValue(json, Person.class);
    System.out.println(grant);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

When you run this code, you’ll see some output like the following:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of 'javamag.jackson.ex5.Person' (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{
  "firstName" : "Grant",
  "lastName" : "Hughes",
  "age" : 19
}"; line: 2, column: 3]
  at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
  at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904)

    // ...

  at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3597)
  at javamag.jackson.ex5.UniversityMain.main(UniversityMain.java:19)

What happened? Recall that ObjectMapper expects getters for serialization—and it wants them to conform to the JavaBeans get/setFoo() convention. ObjectMapper also expects an accessible default constructor, that is, one that takes no parameters.

However, your Person class has none of these things; in fact, all its fields are final. This means setter methods would be totally impossible even if you cheated and added a default constructor to make Jackson happy.

How are you going to resolve this? You certainly aren’t going to warp your application’s object model to comply with the requirements of JavaBeans merely to get serialization to work. Annotations come to the rescue again: You can modify the Person class as follows:

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public Person(@JsonProperty("first_name") String firstName,
                  @JsonProperty("last_name") String lastName,
                  @JsonProperty("age") int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    @JsonProperty("first_name")
    public String firstName() {
        return firstName;
    }

    @JsonProperty("last_name")
    public String lastName() {
        return lastName;
    }

    @JsonProperty("age")
    public int age() {
        return age;
    }

    // other methods elided
}

With these hints, this piece of JSON will be correctly deserialized.

{
    "first_name" : "Grant",
    "last_name" : "Hughes",
    "age" : 19
}

The two key annotations here are

◉ @JsonCreator, which labels a constructor or factory method that will be used to create new Java objects from JSON
◉ @JsonProperty, which maps JSON field names to parameter locations for object creation or for serialization

By adding @JsonProperty to your methods, these methods will be used to provide the values for serialization. If the annotation is added to a constructor or method parameter, it marks where the value for deserialization must be applied.

These annotations allow you to write simple code that can round-trip between JSON and Java objects, as follows:

var mapper = new ObjectMapper()
                    .enable(SerializationFeature.INDENT_OUTPUT);
try {
    var grant = mapper.readValue(json, Person.class);
    System.out.println(grant);

    var parsedJson = mapper.writeValueAsString(grant);
    System.out.println(parsedJson);
} catch (JsonProcessingException e) {
    e.printStackTrace();
}

Example 5: Custom serialization


The first four examples explored two different approaches to Jackson serialization. The simplest approaches required no changes to your code but relied upon the existence of a default constructor and JavaBeans conventions. This may not be convenient for modern applications.

The second approach offered much more flexibility, but it relied upon the use of Jackson annotations, which means your code now has an explicit, direct dependency upon the Jackson libraries.

What if neither of these is an acceptable design constraint? The answer is custom serialization.

Consider the following class, which has no default constructor, immutable fields, a static factory, and Java’s record convention for getters:

public class Person {
    private final String firstName;
    private final String lastName;
    private final int age;

    private Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    public static Person of(String firstName, String lastName, int age) {
        return new Person(firstName, lastName, age);
    }

    public String firstName() {
        return firstName;
    }

    public String lastName() {
        return lastName;
    }

    public int age() {
        return age;
    }

}

Suppose you cannot change this code or introduce a direct coupling to Jackson. That’s a real-world constraint: You may be working with a JAR file and might not have access to the source code of this class.

Here is a solution.

public class PersonSerializer extends StdSerializer<person> {
    public PersonSerializer() {
        this(null);
    }

    public PersonSerializer(Class<person> t) {
        super(t);
    }

    @Override
    public void serialize(Person value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("first_name", value.firstName());
        gen.writeStringField("last_name", value.lastName());
        gen.writeNumberField("age", value.age());
        gen.writeEndObject();
    }
}

Here is the driver code, with exception handling omitted to keep this example simple.

var grant = Person.of("Grant", "Hughes", 19);

var mapper = new ObjectMapper()
                    .enable(SerializationFeature.INDENT_OUTPUT);

var module = new SimpleModule();
module.addSerializer(Person.class, new PersonSerializer());
mapper.registerModule(module);

var json = mapper.writeValueAsString(grant);
System.out.println(json);

This example is very simple; in more-complex scenarios the need arises to traverse an entire object tree, rather than just handling simple string or primitive fields. Those requirements can significantly complicate the process of writing a custom serializer for your domain types.

Example 6: Java 17 records


To finish on an upbeat note: Jackson handles Java records seamlessly. The following code shows how it works; again, exception handling is omitted.

Public record Person(String firstName, String lastName, int age) {}

var grant = new Person("Grant", "Hughes", 19);

var mapper = new ObjectMapper()
                .enable(SerializationFeature.INDENT_OUTPUT);

var json = mapper.writeValueAsString(grant);
System.out.println(json);

var obj = mapper.readValue(json, Person.class);
System.out.println(obj);

This code round-trips the grant object without any problems whatsoever. Jackson’s record-handling capability, which is important for many modern applications, provides yet another great reason to upgrade your Java version and start building your domain models using records and sealed types wherever it is appropriate to do so.

Source: oracle.com