Full featured JSON Parser, Data Model, and JSON Path integration
This project is maintained by xmljim
JEP API Guide » Marshalling and Unmarshalling JSON and POJOs
JSON lends itself ready for mapping to plain old Java objects (POJOs) that can be used for a variety of use cases:
configuration files - store configuration data in JSON and load them into Java configuration classes
data mapping - map JSON data from a JSON source, i.e., a web service to a POJO for data validation and processing
state persistence: Save and/or load application state to JSON and from Java to a persistence store
The org.ghotibeaun.json.converters contains three core interfaces to marshal and unmarshal to and from JSON and POJOs:
In addition to these interfaces, the Converters class is repsonsible for instantiating concrete implementations of these interfaces and contains static methods mapped to all of the interface methods.
The following are some basic rules and assumptions to follow when marshalling and unmarshalling JSON and POJOs. The are some exceptions, and we’ll discuss these in detail further down.
@JSONElement
annotation.
For example, if you have property in your JSON data called name
, there should be a corresponding
Java field called name
with the appropriate type.set[FieldName]
. For the JSON property name
, the setter method should
be named setName
. Converters will first attempt to set/get values from fields on the target class.get[FieldName]
. The one exception is for boolean values, and will also accept is[FieldName]
.
For the JSON property name
, the getter method should be named getName
. Converters will use setter/getter methods on inherited classes.Let’s assume a very basic messenger service that transmits data as a JSON instance:
{
"to": "xml.jim@gmail.com",
"from": "developer@example.com",
"message": "JEP is so cool!"
}
Now assume that we’re consuming that data in our Java application. We can create a POJO that we can use to store and access that data:
public class Message {
private String to;
private String from;
private String message;
/**
* Constructor
*/
public Message() {
}
//create our setters and getters
public void setTo(String to) {
this.to = to;
}
public String getTo() {
return to;
}
public void setFrom(String from) {
this.from = from;
}
public String getFrom() {
return from;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
With JEP, we can use the Converters class to convert our JSON to a new
Message
class instance, using the convertToClass(Class<T> targetClass, JSONObject data)
method:
JSONFactory factory = JSONFactory.newFactory();
JSONParser parser = factory.newParser();
JSONObject messageData = parser.parse();
//convert to Message class
Message message = Converters.convertToClass(Message.class, messageData);
//print message
System.out.println(message.getMessage());
The JSONParser interface also includes methods that support parsing JSON data directly to a POJO:
JSONFactory factory = JSONFactory.newFactory();
JSONParser parser = factory.newParser();
Message message = parser.parse(Paths.get("path/to/json/data"), Message.class);
With JEP, we can use the Converters class to convert our Message
class
instance to a JSON object, using the convertToJSONObject(T source)
method:
Message message = new Message();
message.setTo("developer@example.com");
message.setFrom("xml.jim@gmail.com");
message.setMessage("Hey, thanks!");
JSONObject msg = Converters.convertToJSONObject(message);
System.out.println(msg.getString("message"));
If I serialize the msg
using prettyPrint()
, I’ll get this:
{
"to": "devloper@example.com",
"from": "xml.jim@gmail.com",
"message": "Hey, thanks!"
}
It’s very common for Java applications to work with interfaces or abstract classes. As long as we
have a concrete implementation class, we can convert a JSON instance. Let’s assume we have JSON instance
containing data about teams, in this particular case, baseball teams. We’re working with an interface called Team
that could hold teams of any type (basketball, football, hockey).
We can create an instance of our Team
interface by telling the Converter
what our target class will be
(BaseballTeam.class
).
{
"name": "Boston Red Sox",
"city": "Boston",
"state": "MA",
"stadium": "Fenway Park",
"league": "American League"
}
// Team.java
public interface Team {
void setName(String name);
String getName();
void setCity(String city);
String getCity();
void setState(String state);
String getState();
}
// BaseballTeam.java
public class BaseballTeam implements Team {
private String name;
private String city;
private String state;
private String stadium;
private String league;
public BaseballTeam() {
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void setCity(String city) {
this.city = city;
}
@Override
String getCity() {
return city;
}
@Override
public void setState(String state) {
this.state = state;
}
@Override
public String getState() {
return state;
}
public void setStadium(String stadium) {
this.stadium = stadium;
}
public String getStadium() {
return stadium;
}
public void setLeague(String league) {
this.league = league;
}
public String getLeague() {
return league;
}
}
//create an instance of our Team interface by telling the converter the concrete class to create
Team team = Converters.convertToClass(BaseballTeam.class, teamData);
System.out.println(team.getName());
//We can still access the data from our BaseballTeam class
System.out.println(((BaseballTeam)team).getLeague());
If we round-trip our Team
instance, it will include all of the data for the underlying class, BaseballTeam
.
Likewise, if we created an abstract class that implemented the Team
interface, and refactored the
BaseballTeam
class to extend the abstract class, everything will work the same:
// AbstractTeam.java
public abstract class AbstractTeam implements Team {
private String name;
private String city;
private String state;
public AbstractTeam() {
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void setCity(String city) {
this.city = city;
}
@Override
String getCity() {
return city;
}
@Override
public void setState(String state) {
this.state = state;
}
@Override
public String getState() {
return state;
}
}
//BaseballTeam.java
public class BaseballTeam extends AbstractTeam {
private String stadium;
private String league;
public BaseballTeam() {
super();
}
public void setStadium(String stadium) {
this.stadium = stadium;
}
public String getStadium() {
return stadium;
}
public void setLeague(String league) {
this.league = league;
}
public String getLeague() {
return league;
}
}
We already know that JSON isn’t just keys with primitive values. They can contain lists as well:
{
"continent": "Europe",
"cities": [
"London",
"Paris",
"Barcelona",
"Berlin",
"Warsaw",
"Prague"
]
}
We can create a POJO class with our cities
using Java’s List
interface:
public class Places {
private String continent;
private List<String> cities;
public Places() {
}
public void setContinent(String continent) {
this.continent = continent;
}
public String getContinent() {
return continent;
}
public void setCities(List<String> cities) {
this.cities = cities;
}
public List<String> getCities() {
return cities;
}
}
Now we convert our JSON instance to our Places
class instance:
... //load our JSON...
Places places = Converters.convertToClass(Places.class, classData);
System.out.println(places.getCities().get(3)); //Berlin
To this point, we’ve worked with native JSON value types: Strings, Numbers, Booleans and Nulls.
Even our List example was a simple list of Strings. We can also convert
more complex values store as JSONObject
s in our data:
{
"continent": "Europe",
"location" : {
"city": "London",
"country": "United Kingdom"
}
}
Our POJO might look like this:
// Place.java
public class Place {
private String continent;
private Location location;
public Place() {
}
public void setContinent(String continent) {
this.continent = continent;
}
public String getContinent() {
return continent;
}
public void setLocation(Location location) {
this.location = location;
}
public Location getLocation() {
return location;
}
}
// Location.java
public class Location {
private String city;
private String country;
public Location() {
}
public void setCity(String city) {
this.city = city;
}
public String getCity() {
return city;
}
public void setCountry(String country) {
this.country = country;
}
public String getCountry() {
return country;
}
}
Following our basic rules, as long as the underlying type is a public concrete class with a
default, zero-argument constructor, the JSONConverter
will interpolate the field type and correctly instantiate the
Location
class for the location
property.
... //load our JSON...
Place place = Converters.convertToClass(Place.class, classData);
System.out.println(place.getLocation().getCity()); //London
What if our JSON data includes a list of objects? Converter’s will interrogate the List
’s generic type parameter
to determine which class to instantiate. Again, following our basic rules, as long as
the generic parameter type is a public concrete class with a default, zero-argument constructor, the JSONConverter
will know how to initialize the object.
Let’s assume our JSON data organizes our locations as a list of Place
s:
{
"globalCities": [
{
"continent": "Europe",
"location" : {
"city": "London",
"country": "United Kingdom"
}
}
]
}
Our POJO would look something like this (we’ll borrow our existing Place class we created previously):
public class Atlas {
private List<Place> globalCities;
public Atlas() {
}
public void setGlobalCities(List<Place> globalCities) {
this.globalCities = globalCities;
}
public List<Place> getGlobalCities() {
return globalCities;
}
}
Let’s reorganize the JSON again. This time, we’ll organize by continent
, with each property representing a specific content
containing a list of cities (Location
).
{
"globalCities" : {
"europe": [
{
"city": "London",
"country": "United Kingdom"
},
{
"city": "Paris",
"country": "France"
}
],
"northAmerica": [
{
"city": "New York",
"country": "United States"
},
{
"city": "Toronto",
"country": "Canada"
}
],
"asia": [
{
"city": "Shanghai",
"country": "China"
},
{
"city": "Tokyo",
"country": "Japan"
}
],
"africa": [
{
"city": "Cairo",
"country": "Egypt"
},
{
"city": "Johannesburg",
"country": "South Africa"
}
],
"australia": [
{
"city": "Sydney",
"country": "Australia"
},
{
"city": "Melbourne",
"country": "Australia"
}
],
"southAmerica": [
{
"city": "Sao Paulo",
"country": "Brazil"
},
{
"city": "Buenos Aires",
"country": "Argentina"
}
]
}
}
We would structure our POJOs like this:
// Atlas.java
public class Atlas {
private Continents globalCities;
//getters and setters...
}
// Continents.java
public class Continents {
private List<Location> northAmerica;
private List<Location> europe;
private List<Location> asia;
private List<Location> africa;
private List<Location> australia;
private List<Location> southAmerica;
//getters and setters...
}
There are cases where the basic rules don’t apply. For example, it’s very common to have a fields that are
intrinsically typed to an interface (or a generic of an interface). There are other cases where JSON data types
do not map intrinsically to Java object types. Another perfect example is date objects. Natively, JSON does not
have a data type for dates, so developers will typically use either a Unix-style timestamp (Long
) or a
date/date-time string mapped to the ISO-8601 date format.
Another example might be that POJO and JSON data have different field-to-property mappings, or we want to use a different setter or getter method. Perhaps we simply want to ignore some JSON properties or POJO fields from mapping. All of these scenarios can be accomplished with annotation interfaces that can be applied to different parts of your POJO.
The @TargetClass
annotation allows you
to specify a concrete class to use for a given JSON property containing either a List of objects, or another JSONObject.
This is particularly useful if you wish to use a particular concrete class implementation that overrides the designated
field type, or if the field type is an interface or abstract class.
There is a distinction between assigning a target class parameter on the convertToClass
method and the @TargetClass
annotation:
The target class parameter applies to the entire POJO; the @TargetClass
annotation applies to the specific field or method. So
it’s quite possible to have your POJO using a different class than a field within your POJO.
NOTE: You do not need to use a
@TargetClass
annotation for native JSON types (Strings, Boolean, Numbers, Null), which will throw an exception if you do. You also do not need to use the@TargetType
annotation if your field type is a simple POJO that follows the basic rules
The @TargetClass
annotation requires that you include a valid Class<?>
reference. In the example below, we’ll annotate
a List of Team
s, where Team
is an interface:
// snip
@TargetClass(BaseballTeam.class)
private List<Team> teams;
// snip
This would map to a JSON structure similar to this:
{
"teams": [
{
"name": "Boston Red Sox",
"stadium": "Fenway Park",
"league": "American League",
"worldSeriesWins": [
1903,
1912,
1915,
1916,
1918,
2004,
2007,
2013,
2018
]
},
{
...
}
]
}
This will instruct the JSONConverter to create a BaseballTeam
instance for each item in the JSONArray backed by the property teams
.
Similarly, you can apply the @TargetClass
annotation to a JSONObject:
//snip
@TargetClass(BaseballTeam.class)
private Team team;
//snip
There may be cases where you do not wish to map a POJO field to a JSON property. In this case you can use the
@JSONIgnore
annotation.
@JSONIgnore
private String teamName;
By default the native value()
is set to true
. If you want to change the default behavior, then you can
set the value()
to false
:
@JSONIgnore(false)
private String teamName;
The JSONElement
annotation is used for
mapping JSON properties to a specific property name and/or to a specific setter or getter method in your POJO.
There are three optional fields:
key()
: specifies the specific JSON property key a field is mapped to to, useful when the field and property names do not matchsetterMethod()
: specifies a setter method to use when setting a JSON value. This can be useful for handling
data type issues or for other specific needs where a JSON property value will be processed further. Only needed when you
want to convert a JSONObject to a POJO.getterMethod()
: specifies a getter method to use when retrieving a POJO field value to set to a JSONObject instance. This can
be useful for converting a complex value into a data type that is easily consumed by JSON. Only needed if you’re
planning to convert a POJO to a JSONObject instanceYou do not need to assign a setterMethod()
or getterMethod()
with a key()
. For example, you could set a key()
value
to map a field to particular JSON property name, yet the field name and the setter and getter method names are internally
consistent. Likewise, you could set the setterMethod()
and getterMethod()
properties without setting the key()
value
if the field and property names match.
//example 1 - map a key: maps the 'teams' field to the 'teamArray' property in the JSON data
@JSONElement(key = "teamArray")
private List<Team> teams;
...
public void setTeams(List<Team> team) {...}
public List<Team> getTeams() {...}
//example 2 - map a setter and getter (field and property names match)
@JSONElement(setterMethod = "setTeamList", getterMethod = "getTeamList")
private List<Team> teamArray;
public void setTeamList(List<Team> teams) {...}
public List<Team> getTeamList() {...}
//example 3 - map all three properties
@JSONElement(key = "teamArray", setterMethod = "setTeamList", getterMethod = "getTeamList")
private List<Team> teams;
public void setTeamList(List<Team> teams) {...}
public List<Team> getTeamList() {...}
The @JSONValueConverter
annotation can used
on a field or method that defines a ValueConverter
to be applied to a JSON property value before it is
set to a POJO field. This is only applied when unmarshalling a JSON instance to a POJO. There is a sibling annotation that
is used for converting a Class value into a JSON value: `@ClassValueConverter
IMPORTANT: Do not apply
@JSONValueConverter
or@ClassValueConverter
along with a@TargetClass
annotation. An exception will be thrown if you.
In one way, the JSONValueConverter
behaves in the same way that setterMethod()
does for a @JSONElement
would if you
implemented a conversion there. However, by using the JSONValueConverter
, and specifying a particlular class, you
get the benefit of reuse.
See ValueConverters for more information about implementing them for use by this annotation
The JSONValueConverter
contains two required fields:
converter()
: This is a Class
that must inherit from the ValueConverter
interfaceargs()
: This is String[]
array of any arguments that may be used for this converter. You can specify an empty array {}
if
no arguments are needed.For this example, let’s assume we have a JSON instance that includes a phone
String, and a dateTime
property that stores
a phone call’s date and time they called as a time stamp. We want to handle the phone a as PhoneNumber class that
breaks the phone String into an area code, prefix, and number. For the dateTime
time stamp, we want to convert it
to a LocalDateTime
value.
For our LocalDateTime
converter, we need to specify the format to use to parse the JSON value. Since we’re using a
Long
timestamp format, we’ll use timestamp
as our format argument
@JSONValueConverter(converter = PhoneNumberConverter.class, args = {})
private PhoneNumber phone;
@JSONValueConverter(converter = LocalDateTimeConverter.class, args = {"timestamp"})
private LocalDateTime dateTime;
The @ClassValueConverter
is the sibling annotation for implementing a ValueConverter that is
used when marshalling a POJO into a JSON instance. It includes the same fields as the @JSONValueConverter
.
They are distinct annotations so that you have more control over marshalling and unmarshalling behavior.
ValueConverters are designed to convert values from native JSON primitive types to more complex Java types, and vice-versa. For example, Java includes a rich library of classes for handling temporal data, especially with the java.time
package; JSON, on the other hand, is limited in its capability to store these values. JSON dates and times are typically expressed in two different ways1:
"2020-01-01"
or "2020-01-01T16:24:34.567"
)1577921074567
)All ValueConverters implement the ValueConverter
interface. Implementations are applied to the JSONValueConverter or ClassValueConverter annotation, depending on
the intent of the conversion. ValueConverters are single purpose or unidirectional, meaning that they are only applied to values on a JSON instance OR a POJO.
The AbstractValueConverter
implements the ValueConverter
interface, and also includes convenience methods to ensure that the value’s type is supported, and for accessing any arguments that passed from the
JSONValueConverter or ClassValueConverter annotation. Concrete classes need to only implement the
getConvertedValue(V)
method and
return the intended value type.
In this example, we will create two ValueConverters, one that will convert a JSON timestamp into a LocalDateTime
instance, and second
ValueConverter that will convert a LocalDateTime
instance to a long timestamp value for use in a JSON instance. Each class will extend from
AbstractValueConverter
so that we can take advantage of value checking and easy access to any arguments passed to our ValueConverter.
This ValueConverter will be used to convert a long value to a LocalDateTime instance.
public class JSONValueToLocalDateTime extends AbstractValueConverter<LocalDateTime> {
/**
* Constructor
* @param args any args passed to the Converter
*/
public JSONValueToLocalDateTime(String... args) {
super(args);
}
/**
* Returns the class types that this ValueConverter can process. The AbstractValueConverter
* will evaluate whether the incoming value is one of these types
*/
@Override
public Class<T>[] accepts() {
return new Class[] {Long.class, long.class};
}
/**
* Return the converted value. This method is called after the AbstractValueConverter checks that the
* incoming value type is supported.
*/
@Override
public <V> LocalDateTime getConvertedValue(V value) throws JSONConversionException {
final LocalDateTime ldt = LocalDateTime.ofInstant(Instant.ofEpochMilli((Long)value), ZoneId.systemDefault());
return ldt;
}
}
public class LocalDateTimeToLong extends AbstractValueConverter<Long> {
/**
* Constructor
* @param args any args passed to this ValueConverter
*/
public LocalDateTimeToLong(String... args) {
super(args);
}
/**
* Returns the class types that this ValueConverter can process. The AbstractValueConverter
* will evaluate whether the incoming value is one of these types
*/
@Override
public Class<T>[] accepts() {
return new Class[] {LocalDateTime.class};
}
/**
* Return the converted value. This method is called after the AbstractValueConverter checks that the
* incoming value type is supported.
*/
@Override
public <V> Long getConvertedValue(V value) throws JSONConversionException {
final ZoneOffset offset = ZoneId.systemDefault().getRules().getOffset((LocalDateTime)value);
return ((LocalDateTime)value).toInstant(offset).toEpochMilli();
}
}
Now that we’ve created our ValueConverters, we can apply them to our POJO using the JSONValueConverter and ClassValueConverter annotations on our POJO class variable:
//snip
@JSONValueConverter(converter = JSONValueToLocalDateTime.class, args = {}) //convert a JSON long value to a LocalDateTime
@ClassValueConverter(converter = LocalDateTimeToLong.class, args = {}) //convert a LocalDateTime to a long (timestamp)
private LocalDateTime dateTime;
//snip
We’ve already discuss the Converters class, which provides static methods
to marshal and unmarshal JSON and POJOs. Another common scenario for applying a ValueConverter is converting lists of values to JSONArrays,
and vice versa. Let’s start with an example that is a JSONArray of long values that represent time stamps. We want to convert it to a
List<LocalDateTime>
.
[
1613575337508,
1611212400000
]
Now let’s convert it to the Java List instance:
JSONArray listOfDateTimes = JSONFactory.newFactory().newParser().parse(...);
ValueConverter<LocalDateTime> timestampConverter = new JSONValueToLocalDateTime();
List<LocalDateTime> list = Converters.convertToList(listOfDateTimes, Optional.of(timestampConverter), Optional.empty());
In the example above, we instantiated our ValueConverter that converts timestamp (long) values to LocalDateTime instances. Then
we called the convertToList(JSONArray, Optional<ValueConverter<?>>, Optional<Class<?>>)
method. The Optional<?>
class allows for
either an instance of the designated type, or null. Since we’re passing in our ValueConverter, we use the Optional.of()
method to
assign the the ValueConverter. Likewise, since we’re not using a Target Class, we assign a value of Optional.empty()
.
This can also be used in reverse. Let’s take our List<LocalDateTime>
instance and convert it to a list of timestamps:
ValueConverter<Long> dateTimeConverter = new LocalDateTimeToLong();
JSONArray timestampArray = Converters.convertToJSONArray(list, Optional.of(dateTimeConverter), Optional.empty());
Next: Merging JSON Data
[1] Of course, there are other ways to express date/time values in JSON, but these are the most common