Saturday, April 28, 2012

JSON for polymorphic Java object serialization

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.

5 comments:

Unknown said...

One minor suggestion: you can use ObjectMapper.writeValueAsString(), to remove need for StringWriter.

Peter said...

Hi, great post. I have one problem with JSON, maybe you can help me.
I have Collection of objects (beans), inside each one I have field with link on Set<>. And after I make JSONArray.fromObject(list, config) the inner Collection become empty. But name of this inner coolection appear in result, that meaning filter works. I don't understand why filter makes empty inner Collection? And how I can fix it?

Andriy Redko said...

Hi Peter. Thank you for your comment, what library do you use? Could you please post a code snippet so to understand your use case? Thank you.

Tom Herbert said...

Thank you Andriy, This is the clearest explanation on the topic that I found. All up and running with polymorphic JSON serialization now!

Andriy Redko said...

Hi Tom,

I am glad you've found it useful.
Thank you!

Best Regards,
Andriy Redko