For a long time now JSON is a de facto standard for all kinds of data serialization between client and server. Among other, its strengths are simplicity and human-readability. But with simplicity comes some limitations, one of them I would like to talk about today: storing and retrieving polymorphic Java objects.
Let's start with simple problem: a hierarchy of filters. There is one abstract class AbstractFilter and two subclasses, RegexFilter and StringMatchFilter.
package bean.json.examples; public abstract class AbstractFilter { public abstract void filter(); }
Here is RegexFilter class:
package bean.json.examples; public class RegexFilter extends AbstractFilter { private String pattern; public RegexFilter( final String pattern ) { this.pattern = pattern; } public void setPattern( final String pattern ) { this.pattern = pattern; } public String getPattern() { return pattern; } @Override public void filter() { // Do some work here } }
And here is StringMatchFilter class:
package bean.json.examples; public class StringMatchFilter extends AbstractFilter { private String[] matches; private boolean caseInsensitive; public StringMatchFilter() { } public StringMatchFilter( final String[] matches, final boolean caseInsensitive ) { this.matches = matches; this.caseInsensitive = caseInsensitive; } public String[] getMatches() { return matches; } public void setCaseInsensitive( final boolean caseInsensitive ) { this.caseInsensitive = caseInsensitive; } public void setMatches( final String[] matches ) { this.matches = matches; } public boolean isCaseInsensitive() { return caseInsensitive; } @Override public void filter() { // Do some work here } }
Nothing fancy, pure Java beans. Now what if we need to store list of AbstractFilter instances to JSON, and more importantly, to reconstruct this list back from JSON? Following class Filters demonstrates what I mean:
package bean.json.examples; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; public class Filters { private Collection< AbstractFilter > filters = new ArrayList< AbstractFilter >(); public Filters() { } public Filters( final AbstractFilter ... filters ) { this.filters.addAll( Arrays.asList( filters ) ); } public Collection< AbstractFilter > getFilters() { return filters; } public void setFilters( final Collection< AbstractFilter > filters ) { this.filters = filters; } }
As JSON is textual, platform-independent format, it doesn't carry any type specific information. Thanks to awesome Jackson JSON processor it could be easily done. So let's add Jackson JSON processor to our POM file:
4.0.0 bean.json examples 0.0.1-SNAPSHOT jar UTF-8 org.codehaus.jackson jackson-mapper-asl 1.9.6
Having this step done, we need to tell Jackson that we have an intention to store the type information together with our objects in JSON so it would be possible to reconstruct exact objects from JSON later. Few annotations on AbstractFilter do exactly that.
import org.codehaus.jackson.annotate.JsonSubTypes; import org.codehaus.jackson.annotate.JsonSubTypes.Type; import org.codehaus.jackson.annotate.JsonTypeInfo; import org.codehaus.jackson.annotate.JsonTypeInfo.Id; @JsonTypeInfo( use = Id.NAME ) @JsonSubTypes( { @Type( name = "Regex", value = RegexFilter.class ), @Type( name = "StringMatch", value = StringMatchFilter.class ) } ) public abstract class AbstractFilter { // ... }
And ... that's it! Following helper class does the dirty job of serializing filters to string and deserializing them back from string using Jackson's ObjectMapper:
package bean.json.examples; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import org.codehaus.jackson.map.ObjectMapper; public class FilterSerializer { private final ObjectMapper mapper = new ObjectMapper(); public String serialize( final Filters filters ) { final StringWriter writer = new StringWriter(); try { mapper.writeValue( writer, filters ); return writer.toString(); } catch( final IOException ex ) { throw new RuntimeException( ex.getMessage(), ex ); } finally { try { writer.close(); } catch ( final IOException ex ) { /* Nothing to do here */ } } } public Filters deserialize( final String str ) { final StringReader reader = new StringReader( str ); try { return mapper.readValue( reader, Filters.class ); } catch( final IOException ex ) { throw new RuntimeException( ex.getMessage(), ex ); } finally { reader.close(); } } }
Let's see this in action. Following code example
final String json = new FilterSerializer().serialize( new Filters( new RegexFilter( "\\d+" ), new StringMatchFilter( new String[] { "String1", "String2" }, true ) ) );produces following JSON:
{ "filters": [ {"@type":"Regex","pattern":"\\d+"}, {"@type":"StringMatch","matches":["String1","String2"],"caseInsensitive":true} ] }
As you can see, each entry in "filters" collection has property "@type" which has the value we have specified by annotating AbstractFilter class. Calling new FilterSerializer().deserialize( json ) produces exactly the same Filters object instance.