org.apache.juneau.yaml.proto.package.html Maven / Gradle / Ivy
JSON serialization and parsing support
Table of Contents
-
-
-
1 -JSON support overview
Juneau supports converting arbitrary POJOs to and from JSON using ultra-efficient serializers and parsers.
The JSON serializer converts POJOs directly to JSON without the need for intermediate DOM objects using a
highly-efficient state machine.
Likewise, the JSON parser creates POJOs directly from JSON without the need for intermediate DOM objects.
Juneau can serialize and parse instances of any of the following POJO types:
-
Java primitives and primitive objects (e.g.
String
, Integer
, Boolean
,
Float
).
-
Java Collections Framework objects (e.g.
HashSet
, TreeMap
) containing anything on
this list.
-
Multi-dimensional arrays of any type on this list.
-
Java Beans with properties of any type on this list.
-
Classes with standard transformations to and from
Strings
(e.g. classes containing
toString()
, fromString()
, valueOf()
, constructor(String)
).
-
Non-serializable classes and properties with associated
PojoSwaps
that convert them to
serializable forms.
Refer to POJO Categories
for a complete definition of supported POJOs.
Prerequisites
The Juneau JSON serialization and parsing support does not require any external prerequisites.
It only requires Java 1.6 or above.
1.1 - JSON support overview - example
The example shown here is from the Address Book resource located in the
org.apache.juneau.sample.war
application.
The POJO model consists of a List
of Person
beans, with each Person
containing zero or more Address
beans.
When you point a browser at /sample/addressBook
, the POJO is rendered as HTML:
By appending ?Accept=mediaType&plainText=true
to the URL, you can view the data in
the various supported JSON formats:
Normal JSON
Simple JSON
In addition to serializing POJOs to JSON, Juneau includes support for serializing POJO metamodels to
JSON Schema.
JSON Schema
The JSON data type produced depends on the Java object type being serialized.
-
Primitives and primitive objects are converted to JSON primitives.
-
Beans and Maps are converted to JSON objects.
-
Collections and arrays are converted to JSON arrays.
-
Anything else is converted to JSON strings.
Examples:
POJO type
Example
Serialized form
String
serializer.serialize("foobar" );
'foobar'
Number
serializer.serialize(123);
123
Boolean
serializer.serialize(true );
true
Null
serializer.serialize(null );
null
Beans with properties of any type on this list
serializer.serialize(new MyBean());
{p1:'val1' ,p2:true }
Maps with values of any type on this list
serializer.serialize(new TreeMap());
{key1:'val1' ,key2:true }
Collections and arrays of any type on this list
serializer.serialize(new Object[]{1,"foo" ,true });
[1,'foo',true]
In addition, swaps can be used to convert non-serializable POJOs into serializable forms, such as converting
Calendar
object to ISO8601 strings, or byte []
arrays to Base-64
encoded strings.
These swaps can be associated at various levels:
-
On serializer and parser instances to handle all objects of the class type globally.
-
On classes through the
@Bean
annotation.
-
On bean properties through the
@BeanProperty
annotations.
For more information about transforms, refer to org.apache.juneau.transform.
2 - JsonSerializer class
{@link org.apache.juneau.json.JsonSerializer} is the class used to convert POJOs to JSON.
{@link org.apache.juneau.json.JsonSchemaSerializer} is the class used to generate JSON-Schema from POJOs.
The JSON serializer includes several configurable settings.
Static reusable instances of Json serializers are provided with commonly-used settings:
-
{@link org.apache.juneau.json.JsonSerializer#DEFAULT} - All default settings
-
{@link org.apache.juneau.json.JsonSerializer#DEFAULT_LAX} - Single quotes, only quote attributes when
necessary.
-
{@link org.apache.juneau.json.JsonSerializer#DEFAULT_LAX_READABLE} - Readable output.
Notes about examples
The examples shown in this document will use single-quote, readable settings.
For brevity, the examples will use public fields instead of getters/setters to reduce the size of the examples.
In the real world, you'll typically want to use standard bean getters and setters.
To start off simple, we'll begin with the following simplified bean and build upon it.
public class Person {
// Bean properties
public int id ;
public String name ;
// Bean constructor (needed by parser)
public Person() {}
// Normal constructor
public Person(int id, String name) {
this .id = id;
this .name = name;
}
}
The following code shows how to convert this to simple JSON:
// Use serializer with readable output, simple mode.
JsonSerializer s = JsonSerializer.DEFAULT_LAX_READABLE ;
// Create our bean.
Person p = new Person(1, "John Smith" );
// Serialize the bean to JSON.
String json = s.serialize(p);
We could have also created a new serializer with the same settings using the following code:
JsonSerializer s = new JsonSerializerBuilder().simple().ws().sq().build();
The code above produces the following output:
{
id: 1 ,
name: 'John Smith'
}
2.1 - @Bean and @BeanProperty annotations
The {@link org.apache.juneau.annotation.Bean @Bean} and
{@link org.apache.juneau.annotation.BeanProperty @BeanProperty} annotations are used to customize the
behavior of beans across the entire framework.
They have various uses:
-
Hiding bean properties.
-
Specifying the ordering of bean properties.
-
Overriding the names of bean properties.
-
Associating swaps at both the class and property level (to convert non-serializable POJOs to
serializable forms).
For example, we now add a birthDate
property, and associate a swap with it to transform it to
an ISO8601 date-time string in GMT time.
We'll also add a couple of URI
properties.
By default, Calendars
are treated as beans by the framework, which is usually not how you
want them serialized.
Using swaps, we can convert them to standardized string forms.
public class Person {
// Bean properties
public int id ;
public String name ;
public URI uri ;
public URI addressBookUri ;
@BeanProperty (swap=CalendarSwap.ISO8601DTZ.class ) public Calendar birthDate ;
// Bean constructor (needed by parser)
public Person() {}
// Normal constructor
public Person(int id, String name, String uri, String addressBookUri, String birthDate) throws Exception {
this .id = id;
this .name = name;
this .uri = new URI(uri);
this .addressBookUri = new URI(addressBookUri);
this .birthDate = new GregorianCalendar();
this .birthDate .setTime(DateFormat.getDateInstance (DateFormat.MEDIUM ).parse(birthDate));
}
}
Next, we alter our code to pass in the birthdate:
// Create our bean.
Person p = new Person(1, "John Smith" , "http://sample/addressBook/person/1" ,
"http://sample/addressBook" , "Aug 12, 1946" );
Now when we rerun the sample code, we'll get the following:
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z'
}
Another useful feature is the {@link org.apache.juneau.annotation.Bean#propertyNamer()} annotation that
allows you to plug in your own logic for determining bean property names.
The {@link org.apache.juneau.PropertyNamerDLC} is an example of an alternate property namer.
It converts bean property names to lowercase-dashed format.
Example:
@Bean (propertyNamer=PropertyNamerDLC.class )
public class Person {
...
Results
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
'address-book-uri' : 'http://sample/addressBook' ,
'birth-date' : '1946-08-12T00:00:00Z'
}
2.2 - Collections
In our example, let's add a list-of-beans property to our sample class:
public class Person {
// Bean properties
public LinkedList<Address> addresses = new LinkedList<Address>();
...
}
The Address
class has the following properties defined:
public class Address {
// Bean properties
public URI uri ;
public URI personUri ;
public int id ;
public String street , city , state ;
public int zip ;
public boolean isCurrent ;
}
Next, add some quick-and-dirty code to add an address to our person bean:
// Use serializer with readable output, simple mode.
JsonSerializer s = JsonSerializer.DEFAULT_LAX_READABLE ;
// Create our bean.
Person p = new Person(1, "John Smith" , "http://sample/addressBook/person/1" ,
"http://sample/addressBook" , "Aug 12, 1946" );
Address a = new Address();
a.uri = new URI("http://sample/addressBook/address/1" );
a.personUri = new URI("http://sample/addressBook/person/1" );
a.id = 1;
a.street = "100 Main Street" ;
a.city = "Anywhereville" ;
a.state = "NY" ;
a.zip = 12345;
a.isCurrent = true ;
p.addresses .add(a);
Now when we run the sample code, we get the following:
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z' ,
addresses: [
{
uri: 'http://sample/addressBook/address/1' ,
personUri: 'http://sample/addressBook/person/1' ,
id: 1 ,
street: '100 Main Street' ,
city: 'Anywhereville' ,
state: 'NY' ,
zip: 12345 ,
isCurrent: true
}
]
}
2.3 - JSON-Schema support
Juneau provides the {@link org.apache.juneau.json.JsonSchemaSerializer} class for generating JSON-Schema
documents that describe the output generated by the {@link org.apache.juneau.json.JsonSerializer} class.
This class shares the same properties as JsonSerializer
.
For convenience the {@link org.apache.juneau.json.JsonSerializer#getSchemaSerializer()} method has been
added for creating instances of schema serializers from the regular serializer instance.
Note: As of this writing, JSON-Schema has not been standardized, so the output generated by the
schema serializer may be subject to future modifications.
Lets start with the classes from the previous examples:
public class Person {
// Bean properties
public int id ;
public String name ;
public URI uri ;
public URI addressBookUri ;
@BeanProperty (swap=CalendarSwap.ISO8601DTZ.class ) public Calendar birthDate ;
public LinkedList<Address> addresses = new LinkedList<Address>();
// Bean constructor (needed by parser)
public Person() {}
// Normal constructor
public Person(int id, String name, String uri, String addressBookUri, String birthDate) throws Exception {
this .id = id;
this .name = name;
this .uri = new URI(uri);
this .addressBookUri = new URI(addressBookUri);
this .birthDate = new GregorianCalendar();
this .birthDate .setTime(DateFormat.getDateInstance(DateFormat.MEDIUM ).parse(birthDate));
}
}
public class Address {
// Bean properties
public URI uri ;
public URI personUri ;
public int id ;
public String street , city , state ;
public int zip ;
public boolean isCurrent ;
}
The code for creating our POJO model and generating JSON-Schema is shown below:
// Get the schema serializer for one of the default JSON serializers.
JsonSchemaSerializer s = JsonSerializer.DEFAULT_LAX_READABLE .getSchemaSerializer();
// Create our bean.
Person p = new Person(1, "John Smith" , "http://sample/addressBook/person/1" ,
"http://sample/addressBook" , "Aug 12, 1946" );
Address a = new Address();
a.uri = new URI("http://sample/addressBook/address/1" );
a.personUri = new URI("http://sample/addressBook/person/1" );
a.id = 1;
a.street = "100 Main Street" ;
a.city = "Anywhereville" ;
a.state = "NY" ;
a.zip = 12345;
a.isCurrent = true ;
p.addresses .add(a);
// Get the JSON Schema corresponding to the JSON generated above.
String jsonSchema = s.serialize(p);
Results
{
type: 'object' ,
description: 'org.apache.juneau.sample.Person' ,
properties: {
id: {
type: 'number' ,
description: 'int'
},
name: {
type: 'string' ,
description: 'java.lang.String'
},
uri: {
type: 'any' ,
description: 'java.net.URI'
},
addressBookUri: {
type: 'any' ,
description: 'java.net.URI'
},
birthDate: {
type: 'any' ,
description: 'java.util.Calendar'
},
addresses: {
type: 'array' ,
description: 'java.util.LinkedList<org.apache.juneau.sample.Address>' ,
items: {
type: 'object' ,
description: 'org.apache.juneau.sample.Address' ,
properties: {
uri: {
type: 'any' ,
description: 'java.net.URI'
},
personUri: {
type: 'any' ,
description: 'java.net.URI'
},
id: {
type: 'number' ,
description: 'int'
},
street: {
type: 'string' ,
description: 'java.lang.String'
},
city: {
type: 'string' ,
description: 'java.lang.String'
},
state: {
type: 'string' ,
description: 'java.lang.String'
},
zip: {
type: 'number' ,
description: 'int'
},
isCurrent: {
type: 'boolean' ,
description: 'boolean'
}
}
}
}
}
}
2.4 - Non-tree models and recursion detection
The JSON serializer is designed to be used against POJO tree structures.
It expects that there not be loops in the POJO model (e.g. children with references to parents, etc...).
If you try to serialize models with loops, you will usually cause a StackOverflowError
to
be thrown (if {@link org.apache.juneau.serializer.Serializer#SERIALIZER_maxDepth} is not reached
first).
If you still want to use the JSON serializer on such models, Juneau provides the
{@link org.apache.juneau.serializer.Serializer#SERIALIZER_detectRecursions} setting.
It tells the serializer to look for instances of an object in the current branch of the tree and skip
serialization when a duplicate is encountered.
For example, let's make a POJO model out of the following classes:
public class A {
public B b;
}
public class B {
public C c;
}
public class C {
public A a;
}
Now we create a model with a loop and serialize the results.
// Clone an existing serializer and set property for detecting recursions.
JsonSerializer s = JsonSerializer.DEFAULT_LAX_READABLE .builder().detectRecursions(true ).build();
// Create a recursive loop.
A a = new A();
a.b = new B();
a.b .c = new C();
a.b .c .a = a;
// Serialize to JSON.
String json = s.serialize(a);
What we end up with is the following, which does not serialize the contents of the c
field:
{
b: {
c: {
}
}
}
Without recursion detection enabled, this would cause a stack-overflow error.
Recursion detection introduces a performance penalty of around 20%.
For this reason the setting is disabled by default.
2.5 - Configurable properties
See the following classes for all configurable properties that can be used on this serializer:
-
{@link org.apache.juneau.BeanContext} - Bean context properties.
-
{@link org.apache.juneau.json.JsonSerializerContext} - Serializer context properties.
2.6 - Other notes
-
Like all other Juneau serializers, the JSON serializer is thread safe and maintains an internal cache
of bean classes encountered.
For performance reasons, it's recommended that serializers be reused whenever possible instead of
always creating new instances.
3 - JsonParser class
The {@link org.apache.juneau.json.JsonParser} class is the class used to parse JSON back into POJOs.
The JSON parser supports ALL valid JSON, including:
-
Javascript comments.
-
Single or double quoted values.
-
Quoted (strict) or unquoted (non-strict) attributes.
-
JSON fragments (such as string, numeric, or boolean primitive values).
-
Concatenated strings.
A static reusable instance of JsonParser
is also provided for convenience:
- {@link org.apache.juneau.json.JsonParser#DEFAULT}
Let's build upon the previous example and parse the generated JSON back into the original bean.
We start with the JSON that was generated.
// Use serializer with readable output, simple mode.
JsonSerializer s = JsonSerializer.DEFAULT_LAX_READABLE ;
// Create our bean.
Person p = new Person(1, "John Smith" , "http://sample/addressBook/person/1" ,
"http://sample/addressBook" , "Aug 12, 1946" );
Address a = new Address();
a.uri = new URI("http://sample/addressBook/address/1" );
a.personUri = new URI("http://sample/addressBook/person/1" );
a.id = 1;
a.street = "100 Main Street" ;
a.city = "Anywhereville" ;
a.state = "NY" ;
a.zip = 12345;
a.isCurrent = true ;
p.addresses .add(a);
// Serialize the bean to JSON.
String json = s.serialize(p);
This code produced the following:
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z' ,
addresses: [
{
uri: 'http://sample/addressBook/address/1' ,
personUri: 'http://sample/addressBook/person/1' ,
id: 1 ,
street: '100 Main Street' ,
city: 'Anywhereville' ,
state: 'NY' ,
zip: 12345 ,
isCurrent: true
}
]
}
The code to convert this back into a bean is:
// Parse it back into a bean using the reusable JSON parser.
Person p = JsonParser.DEFAULT .parse(json, Person.class );
// Render it back as JSON.
json = JsonSerializer.DEFAULT_LAX_READABLE .serialize(p);
We print it back out to JSON to show that all the data has been preserved:
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z' ,
addresses: [
{
uri: 'http://sample/addressBook/address/1' ,
personUri: 'http://sample/addressBook/person/1' ,
id: 1 ,
street: '100 Main Street' ,
city: 'Anywhereville' ,
state: 'NY' ,
zip: 12345 ,
isCurrent: true
}
]
}
3.1 - Parsing into generic POJO models
The JSON parser is not limited to parsing back into the original bean classes.
If the bean classes are not available on the parsing side, the parser can also be used to
parse into a generic model consisting of Maps
, Collections
, and primitive
objects.
You can parse into any Map
type (e.g. HashMap
, TreeMap
), but
using {@link org.apache.juneau.ObjectMap} is recommended since it has many convenience methods
for converting values to various types.
The same is true when parsing collections. You can use any Collection (e.g. HashSet
,
LinkedList
) or array (e.g. Object[]
, String[]
,
String[][]
), but using {@link org.apache.juneau.ObjectList} is recommended.
When the map or list type is not specified, or is the abstract Map
, Collection
,
or List
types, the parser will use ObjectMap
and ObjectList
by
default.
Starting back with our original JSON:
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z' ,
addresses: [
{
uri: 'http://sample/addressBook/address/1' ,
personUri: 'http://sample/addressBook/person/1' ,
id: 1 ,
street: '100 Main Street' ,
city: 'Anywhereville' ,
state: 'NY' ,
zip: 12345 ,
isCurrent: true
}
]
}
We can parse this into a generic ObjectMap
:
// Parse JSON into a generic POJO model.
ObjectMap m = JsonParser.DEFAULT .parse(json, ObjectMap.class );
// Convert it back to JSON.
String json = JsonSerializer.DEFAULT_LAX_READABLE .serialize(m);
What we end up with is the exact same output.
Even the numbers and booleans are preserved because they are parsed into Number
and
Boolean
objects when parsing into generic models.
{
id: 1 ,
name: 'John Smith' ,
uri: 'http://sample/addressBook/person/1' ,
addressBookUri: 'http://sample/addressBook' ,
birthDate: '1946-08-12T00:00:00Z' ,
addresses: [
{
uri: 'http://sample/addressBook/address/1' ,
personUri: 'http://sample/addressBook/person/1' ,
id: 1 ,
street: '100 Main Street' ,
city: 'Anywhereville' ,
state: 'NY' ,
zip: 12345 ,
isCurrent: true
}
]
}
Once parsed into a generic model, various convenience methods are provided on the ObjectMap
and ObjectList
classes to retrieve values:
// Parse JSON into a generic POJO model.
ObjectMap m = JsonParser.DEFAULT .parse(json, ObjectMap.class );
// Get some simple values.
String name = m.getString("name" );
int id = m.getInt("id" );
// Get a value convertable from a String.
URI uri = m.get(URI.class , "uri" );
// Get a value using a swap.
CalendarSwap swap = new CalendarSwap.ISO8601DTZ();
Calendar birthDate = m.get(swap, "birthDate" );
// Get the addresses.
ObjectList addresses = m.getObjectList("addresses" );
// Get the first address and convert it to a bean.
Address address = addresses.get(Address.class , 0);
As a general rule, parsing into beans is often more efficient than parsing into generic models.
And working with beans is often less error prone than working with generic models.
3.2 - Configurable properties
See the following classes for all configurable properties that can be used on this parser:
-
{@link org.apache.juneau.BeanContext} - Bean context properties.
-
{@link org.apache.juneau.json.JsonParserContext} - Serializer context properties.
3.3 - Other notes
-
Like all other Juneau parsers, the JSON parser is thread safe and maintains an internal cache of bean
classes encountered.
For performance reasons, it's recommended that parser be reused whenever possible instead of always
creating new instances.
*** fín ***