Custom Field Name with @JsonProperty
1. Introduction
Custom field name with @JsonProperty
is very useful when mapping Java fields to JavaScript Object Notation (JSON) properties and vice versa. JSON is commonly used in web applications to transmit data and the clients and servers can be written in different programming languages with different naming conventions. For example, JSON adapts the “snake_case” for the property name while Java uses the “camelCase” format for the field name. The @JsonProperty annotation from the Jackson library maps Java fields to JSON properties that meet both naming conventions. In this example, I will demonstrate custom field name with @JsonProperty
annotation usage from the Jackson library.
2. Setup
In this step, I will create a Maven project which depends on Jackson
, Junit
, and Lombok
. The Lombok is included to reduce the boilerplate code.
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-jsonproperty</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> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.32</version> <scope>provided</scope> </dependency> </dependencies> </project>
3. Custom Field Name with @JsonProperty
In this step, I will create three POJO classes which annotate a custom field name with jsonproperty
annotation at the field
, getter
, and constructor
positions.
Customer.java:
@JsonProperty
is placed at thedataType
field.Order.java
:@JsonProperty
is placed at thegetName()
method.Person.java
@JsonProperty
is placed at thePerson
constructor.
3.1. Custom Field Name with @JsonProperty at Field
In this step, I will create a Customer.java
class which annotates the @JsonProperty
in the dataType
field and maps the dataType
field to JSON type
property. Note: cannot name the field name as type
directly as it is a reserved keyword in Java.
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; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; @Data @NoArgsConstructor @RequiredArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) public class Customer implements Serializable { private static final long serialVersionUID = -1319171342148919329L; @JsonProperty("type") private String dataType; @NonNull private String email; private int id; private List<Order> orders; private Person person; public void addOrder(Order order) { if (this.orders == null) { this.orders = new ArrayList<>(); } this.orders.add(order); } }
- Line 22: maps the Java
dataType
field to the JSONtype
property. - Line 25: marks the
email
field as@NonNull
value.
3.2 Custom Field Name with @JsonProperty at Getter
In this step, I will create an Order.java
class which annotates the @JsonProperty
at the getProductName
method. The JSON property “product_name
” maps to the Java “productName
” field.
Order.java
package org.zheng.demo.data; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class Order { private String productName; private int quantity; public Order() { super(); } public Order(String productName, int quantity) { super(); this.productName = productName; this.quantity = quantity; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Order other = (Order) obj; return Objects.equals(productName, other.productName) && quantity == other.quantity; } @JsonProperty("product_name") public String getProductName() { return productName; } public int getQuantity() { return quantity; } @Override public int hashCode() { return Objects.hash(productName, quantity); } public void setProductName(String productName) { this.productName = productName; } public void setQuantity(int quantity) { this.quantity = quantity; } }
- Line 37: maps the Java “
productName
” field to “product_name
” JSON property.
3.3 Custom Field Name with @JsonProperty at Constructor
In this step, I will create a Person.java
class that annotates the @JsonProperty
at the Person
constructor. It also marks the product_name
as required.
Person.java
package org.zheng.demo.data; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class Person { private int age; @JsonProperty("person_name") private String name; @JsonCreator public Person(@JsonProperty(value = "person_name", required = true) String name, @JsonProperty("age") int age) { this.name = name; this.age = age; } }
- Line 13: annotates the field to “
person_name
“. - Line 16: marks the constructor used for the deserialization process.
- Line 17: maps the JSON
person_name
property to Java name field and marks it as a required field.
4. Test Person
In this step, I will create a Junit TestPerson.java
which has the following three tests:
test_happy_path
– it serialized and deserialized data as expected.test_required_throw_exception_if_not_there
– when the requiredperson_name
property is missing, then throwsMismatchedInputException
.test_required_with_null_is_ok
– when the requiredperson_name
property has anull
value, then it maps tonull
. Note: therequired=true
is different from@NonNull
annotation.
TestPerson.java
package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.zheng.demo.data.Person; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; class TestPerson { private ObjectMapper ob = new ObjectMapper(); @Test void test_happy_path() { // name and age have value String json = "{\"person_name\":\"John Doe\",\"age\":30}"; try { Person pers = ob.readValue(json, Person.class); assertEquals("John Doe", pers.getName()); assertEquals(30, pers.getAge()); } catch (JsonProcessingException e) { e.printStackTrace(); } } @Test void test_required_throw_exception_if_not_there() { // name is not there, String jsonWOName = "{\"name1\":\"John Doe\",\"age\":30}"; MismatchedInputException expectedException = assertThrows(MismatchedInputException.class, () -> ob.readValue(jsonWOName, Person.class)); assertTrue(expectedException.getMessage().contains("Missing required creator property 'person_name'") ); } @Test void test_required_with_null_is_ok() { // has name but the value is null String jsonNullName = "{\"person_name\":null,\"age\":30}"; try { Person pers2 = ob.readValue(jsonNullName, Person.class); assertNull(pers2.getName()); assertEquals(30, pers2.getAge()); } catch (JsonProcessingException e) { e.printStackTrace(); } } }
- Line 22: the
person_name
maps to a non-null value. - line 36: throw an exception if JSON does not have required
person_name.
- line 45: the
person_name
maps anull
value.
5. Test Order
5.1 Order.json
In this step, I will create an Order.json
file and will be used in step 5.2.
Order.json
{ "quantity" : 10, "product_name" : "test" }
- the
product_name
is mapped toproductName
.
5.2 TestOrder.java
In this step, I will create a Junit TestOrder.java
which has the following two tests:
test_ignore_unknown
– the unknown JSON properties are ignored.test_JsonProperty_getter_happypath
– JSON properties are mapped to POJO fields as expected with the@JsonProperty
set at thegetName
method.
TestOrder.java
package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import java.io.File; import java.io.IOException; import org.junit.jupiter.api.Test; import org.zheng.demo.data.Order; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; class TestOrder { private ObjectMapper ob = new ObjectMapper(); @Test void test_ignore_unknown() { // name and age have value String json = "{\"productName\":\"PC\",\"quantity\":3}"; try { Order order = ob.readValue(json, Order.class); assertNull(order.getProductName()); assertEquals(3, order.getQuantity()); } catch (JsonProcessingException e) { e.printStackTrace(); } } @Test void test_JsonProperty_getter_happypath() { File file = new File("src/test/resources/order.json"); try { Order order = ob.readValue(file, Order.class); assertEquals("test", order.getProductName()); assertEquals(10, order.getQuantity()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
- Line 22: the JSON has “
productName
” which is unknown since@JsonProperty
maps to “product_name
“. - Line 25: the deserialized
order
object has anull
value in theproductName
field.
6. Test Customer
In this step, I will create a Junit TestCustomer.java
which has the following seven tests:
test_customer_full_happyPath
– read thecustomer_full.json
and map to Java POJO as expected.test_customer_no_email_throw_JsonMappingException
– throwJsonMappingException
when@NonNull
email is not there.test_customer_no_person_is_ok
– although theperson
class requires aperson_name
, but thecustomer
class does not require aperson
, so acustomer
‘sperson
is mapped tonull
.test_customer_null_email_throw_JsonMappingException
– because theemail
field is defined as non-null, so an exception is thrown when theob.readValue()
method is called.test_customer_person_no_name_throw_MismatchedInputException
– When a customer JSON contains aperson
to thecustomer
, butperson_name
is missing, then an exception is thrown as theperson_name
is required.test_customer_wo_email_personName_throws_MismatchedInputException
– when non-null email is missing and requiredperson_name
is missing, then missingperson_name
exception is thrown.test_JsonProperty_field
– verify that the@email
field annotated at field level works as expected.
TestCustomer.java
package org.zheng.demo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.File; import java.io.IOException; 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.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; class TestCustomer { private ObjectMapper ob = new ObjectMapper(); @Test void test_customer_full_happyPath() { File file = new File("src/test/resources/customer_full.json"); try { Customer readCust = ob.readValue(file, Customer.class); assertEquals("Mary", readCust.getPerson().getName()); assertEquals(50, readCust.getPerson().getAge()); assertEquals("test@test.com", readCust.getEmail()); assertEquals("major", readCust.getDataType()); assertEquals(2, readCust.getId()); assertEquals(2, readCust.getOrders().size()); } catch (JsonProcessingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } @Test void test_customer_no_email_throw_JsonMappingException() { File file = new File("src/test/resources/customer_no_email.json"); JsonMappingException expectedException = assertThrows(JsonMappingException.class, () -> { ob.readValue(file, Customer.class); }); assertTrue(expectedException.getMessage().contains("email is marked non-null but is null")); } @Test void test_customer_no_person_is_ok() { File file = new File("src/test/resources/customer_no_person.json"); try { Customer cust = ob.readValue(file, Customer.class); assertNull(cust.getPerson()); } catch (IOException e) { e.printStackTrace(); } } @Test void test_customer_null_email_throw_JsonMappingException() { File file = new File("src/test/resources/customer_null_email.json"); JsonMappingException expectedException = assertThrows(JsonMappingException.class, () -> { ob.readValue(file, Customer.class); }); assertTrue(expectedException.getMessage().contains("email is marked non-null but is null")); } @Test void test_customer_person_no_name_throw_MismatchedInputException() { File file = new File("src/test/resources/customer_person_no_name.json"); MismatchedInputException expectedException = assertThrows(MismatchedInputException.class, () -> ob.readValue(file, Customer.class)); assertTrue(expectedException.getMessage().contains("Missing required creator property 'person_name' ")); } @Test void test_customer_wo_email_personName_throws_MismatchedInputException() { File file = new File("src/test/resources/customer_no_email_no_personName.json"); MismatchedInputException expectedException = assertThrows(MismatchedInputException.class, () -> ob.readValue(file, Customer.class)); assertTrue(expectedException.getMessage().contains("Missing required creator property 'person_name' (index 0)")); } @Test void test_JsonProperty_field() { Customer customer = new Customer("test@test.com"); customer.setDataType("major"); customer.setId(2); Order order = new Order("test", 10); customer.addOrder(order); Order order2 = new Order("PS", 20); customer.addOrder(order2); try { String jsonStr = ob.writerWithDefaultPrettyPrinter().writeValueAsString(customer); System.out.println(jsonStr); Customer readCust = ob.readValue(jsonStr, Customer.class); assertTrue(customer.equals(readCust)); assertEquals("major", readCust.getDataType()); assertEquals("test@test.com", readCust.getEmail()); assertEquals(2, readCust.getId()); assertEquals(2, readCust.getOrders().size()); assertEquals("test", readCust.getOrders().get(0).getProductName()); assertEquals(10, readCust.getOrders().get(0).getQuantity()); } catch (JsonProcessingException e) { e.printStackTrace(); } } }
- Line 26: see
customer_full.json
at step 6.1. - Line 45: see
customer_no_email.json
at step 6.2. - Line 57: see
customer_no_person.json
at step 6.4. - Line 69: see
customer_null_email.json
at step 6.5. - Line 81: see
customer_person_no_name.json
at step 6.6. - Line 90: see
customer_no_email_no_personName.json
at step 6.3.
6.1 customer_full.json
In this step, I will create a customer_full.json
file which contains all the necessary data properties. The file is used at the test_customer_full_happyPath
method.
customer_full.json
{ "id" : 2, "person" : { "person_name":"Mary", "age":50}, "orders" : [ { "quantity" : 10, "product_name" : "test" }, { "quantity" : 20, "product_name" : "PS" } ], "type" : "major", "email" : "test@test.com" }
- Line 4: the
person_name
property maps toPerson.name
. - Line 8,11: the
product_name
property maps toOrder.productName
. - Line 13: the
type
property maps toCustomer.dataType
.
6.2 customer_no_email.json
In this step, I will create a customer_no_email
.json file that contains a customer JSON without the required email
property. It’s used at test_customer_no_email_throw_JsonMappingException
.
customer_no_email.json
{ "id" : 2, "person" : null, "orders" : [ { "quantity" : 10, "product_name" : "test" }, { "quantity" : 20, "product_name" : "PS" } ], "type" : "major", "email" : null }
- Line 12: when the
email
property isnull
, it will throw aJsonMappingException
as it is@NonNull
at step 3.1.
6.3 customer_no_email_no_personName.json
In this step, I will create a customer_no_email_no_personName.json
file that contains a customer JSON without the required email
property and person_name
.
customer_no_email_no_personName.json
{ "id" : 2, "person" : { "name":"Mary", "age":50}, "orders" : [ { "quantity" : 10, "product_name" : "test" }, { "quantity" : 20, "product_name" : "PS" } ], "type" : "major" }
- Line 4: the
perons
.name
should beperson_name
based on the@JsonProperty
at step 3.3.
6.4 customer_no_person.json
In this step, I will create a customer_no_person.json
file that contains a customer JSON without a person
.
customer_no_person.json
{ "id" : 2, "orders" : [ { "quantity" : 10, "product_name" : "test" }, { "quantity" : 20, "product_name" : "PS" } ], "type" : "major", "email" : "test@test.com" }
- It should map to a
customer
object without any issue as seen intest_customer_no_person_is_ok
.
6.5 customer_null_email.json
In this step, I will create a customer_null_email.json
file that contains a custom JSON with null
email
property.
customer_null_email.json
{ "id" : 2, "person" : null, "orders" : [ { "quantity" : 10, "product_name" : "test" }, { "quantity" : 20, "product_name" : "PS" } ], "type" : "major", "email" : null }
- Line 12: it throws a
JsonMappingException
as the@NonNull email
isnull
.
6.6 customer_person_no_name.json
In this step, I will create a customer_person_no_name.json
file that contains a JSON for the customer without the person’s name
property.
customer_person_no_name.json
{ "id": 2, "person": { "name": "test", "age": 30 }, "orders": [ { "quantity": 10, "product_name": "test" }, { "quantity": 20, "product_name": "PS" } ], "type": "major", "email": "test@test.com" }
- Line 4: it throws
MismatchedInputException
as thePerson's name
should beperson_name
as defined at step 3.3.
Run the Junit tests and all passed.
7. Conclusion
As you see in this example, I demonstrated how to serialize and deserialize via Jackson custom field name with @JsonProperty
annotation. We can annotate Java POJO fields with the @JsonProperty
annotation at the field
, getter
, or constructor
to map the Java fields to JSON properties.
Please note that there are two Jackson library versions: version 2 is provided by fasterxml and version 1 is provided by codehaus. Make sure that @JsonProperty
is imported from the correct package if the project contains both versions of Jackson libraries.
8. Download
This was an example of a Maven project which demonstrated how to customize field name with the @JsonpPoperty
annotation.
You can download the full source code of this example here: Custom field name with @JsonProperty