flexjson.JSONDeserializer Maven / Gradle / Ivy
package flexjson;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import flexjson.factories.ClassLocatorObjectFactory;
import flexjson.factories.ExistingObjectFactory;
import flexjson.locators.StaticClassLocator;
/**
*
* JSONDeserializer takes as input a json string and produces a static typed object graph from that
* json representation. By default it uses the class property in the json data in order to map the
* untyped generic json data into a specific Java type. However, you are limited to only json strings
* with class information embedded when resolving it into a Java type. But, for now let's just look at
* the simplest case of class attributes in your json. We'll look at how {@link JSONSerializer} and
* JSONDeserializer pair together out of the box.
*
*
* Say we have a simple object like Hero (see the superhero package under the test and mock).
* To create a json represenation of Hero we'd do the following:
*
*
*
* Hero harveyBirdman = new Hero("Harvey Birdman", new SecretIdentity("Attorney At Law"), new SecretLair("Sebben & Sebben") );
* String jsonHarvey = new JSONSerialize().serialize(hero);
*
*
* Now to reconsitute Harvey to fight for the law we'd use JSONDeserializer like so:
*
*
* Hero hero = new JSONDeserializer().deserialize( jsonHarvey );
*
*
* Pretty easy when all the type information is included with the JSON data. Now let's look at the more difficult
* case of how we might reconstitute something missing type info.
*
*
* Let's exclude the class attribute in our json like so:
*
*
*
* String jsonHarvey = new JSONSerialize().exclude("*.class").serialize(hero);
*
*
* The big trick here is to replace that type information when we instantiate the deserializer.
* To do that we'll use the {@link flexjson.JSONDeserializer#use(String, Class)} method like so:
*
*
* Hero hero = new JSONDeserializer().use( null, Hero.class ).deserialize( jsonHarvey );
*
*
* Like riding a horse with no saddle without our type information. So what is happening here is we've registered
* the Hero class to the root of the json. The {@link flexjson.JSONDeserializer#use(String, Class)} method uses
* the object graph path to attach certain classes to those locations. So, when the deserializer is deserializing
* it knows where it is in the object graph. It uses that graph path to look up the java class it should use
* when reconstituting the object.
*
*
* Notice that in our json you'd see there is no type information in the stream. However, all we had to do is point
* the class at the Hero object, and it figured it out. That's because it uses the target type (in this case Hero)
* to figure out the other types by inspecting that class. Meaning notice that we didn't have to tell it about
* SecretLair or SecretIdentity. That's because it can figure that out from the Hero class.
*
*
* Pretty cool. Where this fails is when we starting working with interfaces, abstract classes, and subclasses.
* Yea our friend polymorphism can be a pain when deserializing. Why? Well if you haven't realized by now
* inspecting the type from our target class won't help us because either it's not a concrete class or we
* can't tell the subclass by looking at the super class alone. Next section we're going to stand up on our
* bare back horse. Ready? Let's do it.
*
*
* Before we showed how the {@link flexjson.JSONDeserializer#use(String, Class)} method would allow us to
* plug in a single class for a given path. That might work when you know exactly which class you want to
* instantiate, but when the class type depends on external factors we really need a way to specify several
* possibilities. That's where the second version of {@link flexjson.JSONDeserializer#use(String, ClassLocator)}
* comes into play. {@link flexjson.ClassLocator} allow you to use a stradegy for finding which java Class
* you want to attach at a particular object path.
*
*
* {@link flexjson.JSONDeserializer#use(String, ClassLocator)} have access to the intermediate form of
* the object as a Map. Given the Map at the object path the ClassLocator figures out which Class
* Flexjson will bind the parameters into that object.
*
*
* Let's take a look at how this can be done using our Hero class. All Heros have a list of super powers.
* These super powers are things like X Ray Vision, Heat Vision, Flight, etc. Each super power is represented
* by a subclass of SuperPower. If we serialize a Hero without class information embedded we'll need a way to
* figure out which instance to instantiate when we deserialize. In this example I'm going to use a Transformer
* during serialization to embed a special type information into the object. All this transformer does is strip
* off the package information on the class property.
*
*
* String json = new JSONSerializer()
* .include("powers.class")
* .transform( new SimpleTransformer(), "powers.class")
* .exclude("*.class")
* .serialize( superhero );
* Hero hero = new JSONDeserializer()
* .use("powers.class", new PackageClassLocator())
* .deserialize( json );
*
*
*
*
*
* All objects that pass through the deserializer must have a no argument constructor. The no argument
* constructor does not have to be public. That allows you to maintain some encapsulation. JSONDeserializer
* will bind parameters using setter methods of the objects instantiated if available. If a setter method
* is not available it will using reflection to set the value directly into the field. You can use setter
* methods transform the any data from json into the object structure you want. That way json structure
* can be different from your Java object structure. The works very much in the same way getters do for
* the {@link flexjson.JSONSerializer}.
*
*
* Collections and Maps have changed the path structure in order to specify concrete classes for both
* the Collection implementation and the contained values. Normally you would use generics to specify
* the concrete class to load. However, if you're contained class is an interface or abstract class
* then you'll need to define those concrete classes using paths. To specify the concrete class for
* a Collection use the path to the collection. To specify the contained instance's concrete class
* append "values" onto the path. For example, if your collection path is "person.friends" you can
* specify the collection type using:
*
*
* new JSONDeserializer().use("person.friends", ArrayList.class).use("person.friends.values", Frienemies.class)
*
*
* Notice that append "values" onto the "person.friends" to specify the class to use inside the
* Collection. Maps have both keys and values within them. For Maps you can specify those by
* appending "keys" and "values" to the path.
*
*
* Now onto the advanced topics of the deserializer. {@link flexjson.ObjectFactory} interface is the
* underpinnings of the deserializer. All object creation is controlled by ObjectFactories. By default
* there are many ObjectFactories registered to handle all of the default types supported. However, you
* can add your own implementations to handle specialized formats. For example, say you've encoded your
* Dates using yyyy.MM.dd. If you want to read these into java.util.Date objects you can register a
* {@link flexjson.transformer.DateTransformer} to deserialize dates into Date objects.
*
*/
public class JSONDeserializer
{
private Map typeFactories= new HashMap();
private Map pathFactories= new HashMap();
public static ObjectBinder lastObjectBinder;
public JSONDeserializer()
{
}
/**
* Deserialize the given json formatted input into a Java object.
*
* @param input a json formatted string.
* @return an Java instance deserialized from the json input.
*/
public T deserialize(String input)
{
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue());
}
/**
* Same as {@link #deserialize(String)}, but uses an instance of
* java.io.Reader as json input.
*
* @param input the stream where the json input is coming from.
* @return an Java instance deserialized from the java.io.Reader's input.
*/
public T deserialize(Reader input)
{
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue());
}
/**
* Deserialize the given json input, and use the given Class as
* the type of the initial object to deserialize into. This object
* must implement a no-arg constructor.
*
* @param input a json formatted string.
* @param root a Class used to create the initial object.
* @return the object created from the given json input.
*/
public T deserialize(String input, Class root)
{
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue(), root);
}
/**
* Same as {@link #deserialize(java.io.Reader, Class)}, but uses an instance of
* java.io.Reader as json input.
*
* @param input the stream where the json input is coming from.
* @param root a Class used to create the initial object.
* @return an Java instance deserialized from the java.io.Reader's input.
*/
public T deserialize(Reader input, Class root)
{
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue(), root);
}
/**
* Deserialize the given json input, and use the given ObjectFactory to
* create the initial object to deserialize into.
*
* @param input a json formatted string.
* @param factory an ObjectFactory used to create the initial object.
* @return the object created from the given json input.
*/
public T deserialize(String input, ObjectFactory factory)
{
use((String) null, factory);
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue());
}
/**
* Same as {@link #deserialize(String, ObjectFactory)}, but uses an instance of
* java.io.Reader as json input.
*
* @param input the stream where the json input is coming from.
* @param factory an ObjectFactory used to create the initial object.
* @return an Java instance deserialized from the java.io.Reader's input.
*/
public T deserialize(Reader input, ObjectFactory factory)
{
use((String) null, factory);
ObjectBinder binder= createObjectBinder();
return (T) binder.bind(new JSONTokener(input).nextValue());
}
/**
* Deserialize the given input into the existing object target.
* Values in the json input will overwrite values in the
* target object. This means if a value is included in json
* a new object will be created and set into the existing object.
*
* @param input a json formatted string.
* @param target an instance to set values into from the json string.
* @return will return a reference to target.
*/
public T deserializeInto(String input, T target)
{
return deserialize(input, new ExistingObjectFactory(target));
}
/**
* Same as {@link #deserializeInto(String, Object)}, but uses an instance of
* java.io.Reader as json input.
*
* @param input the stream where the json input is coming from.
* @param target an instance to set values into from the json string.
* @return will return a reference to target.
*/
public T deserializeInto(Reader input, T target)
{
return deserialize(input, new ExistingObjectFactory(target));
}
public JSONDeserializer use(String path, ClassLocator locator)
{
pathFactories.put(Path.parse(path), new ClassLocatorObjectFactory(locator));
return this;
}
public JSONDeserializer use(String path, Class clazz)
{
return use(path, new StaticClassLocator(clazz));
}
public JSONDeserializer use(Class clazz, ObjectFactory factory)
{
typeFactories.put(clazz, factory);
return this;
}
public JSONDeserializer use(String path, ObjectFactory factory)
{
pathFactories.put(Path.parse(path), factory);
return this;
}
public JSONDeserializer use(ObjectFactory factory, String... paths)
{
for (String p : paths)
{
use(p, factory);
}
return this;
}
private ObjectBinder createObjectBinder()
{
ObjectBinder binder= new ObjectBinder();
lastObjectBinder= binder;
for (Object clazz : typeFactories.keySet())
{
binder.use(clazz, typeFactories.get(clazz));
}
for (Path p : pathFactories.keySet())
{
binder.use(p, pathFactories.get(p));
}
return binder;
}
}