Search code examples
javajsoneclipselinkmarshallingmoxy

EclipseLink MOXy JSON Serialization


I have got a sample class:

class Zoo {
    public Collection<? extends Animal> animals;
}

When serialized with MOXy, I am getting:

{
    "bird": [
        {
            "name": "bird-1",
            "wingSpan": "6 feets",
            "preferredFood": "food-1"
        }
    ],
    "cat": [
        {
            "name": "cat-1",
            "favoriteToy": "toy-1"
        }
    ],
    "dog": [
        {
            "name": "dog-1",
            "breed": "bread-1",
            "leashColor": "black"
        }
    ]
}

Why is it using array indicators "[]", while bird, cat, and dog are not arrays? Second, is there a way to get rid of "bird", "cat", and "dog"?

In other words, I am trying to get to:

{
        {
            "name": "bird-1",
            "wingSpan": "6 feets",
            "preferredFood": "food-1"
        }
    ,
        {
            "name": "cat-1",
            "favoriteToy": "toy-1"
        }
    ,
        {
            "name": "dog-1",
            "breed": "bread-1",
            "leashColor": "black"
        }
}

Thanks, Behzad


Solution

  • QUESTION #1

    Why is it using array indicators "[]", while bird, cat, and dog are not arrays?

    To get this JSON representation you have mapped your model with the @XmlElementRef annotation which tells JAXB to use the value of the @XmlRootElement annotation as the inheritance indicator. With MOXy's JSON binding these become keys. We make the value of these keys JSON arrays since keys are not allowed to repeat.

    Zoo

    In your model you have the @XmlElementRef annotation on your animals field/property.

    import java.util.Collection;
    import javax.xml.bind.annotation.XmlElementRef;
    
    class Zoo {
        @XmlElementRef
        public Collection<? extends Animal> animals;
    }
    

    Animal

    import javax.xml.bind.annotation.*;
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlSeeAlso({Bird.class, Cat.class, Dog.class})
    public abstract class Animal {
    
        private String name;
    
    }
    

    Bird

    On each of your subclasses you have an @XmlRootElement annotation.

    import javax.xml.bind.annotation.XmlRootElement;
    
    @XmlRootElement
    public class Bird extends Animal {
    
        private String wingSpan;
        private String preferredFood;
    
    }
    

    input.json/Output

    {
       "bird" : [ {
          "name" : "bird-1",
          "wingSpan" : "6 feets",
          "preferredFood" : "food-1"
       } ],
       "cat" : [ {
          "name" : "cat-1",
          "favoriteToy" : "toy-1"
       } ],
       "dog" : [ {
          "name" : "dog-1",
          "breed" : "bread-1",
          "leashColor" : "black"
       } ]
    }
    

    For More Information


    QUESTION #2

    Second, is there a way to get rid of "bird", "cat", and "dog"?

    You are going to need some sort of inheritance indicator to represent the various subclasses.

    OPTION #1 - @XmlDescriminatorNode/@XmlDescriminatorValue

    Here I do this using MOXy's @XmlDescriminatorNode/@XmlDescriminatorValue annotations.

    Zoo

    import java.util.Collection;
    
    class Zoo {
        public Collection<? extends Animal> animals;
    }
    

    Animal

    import javax.xml.bind.annotation.*;
    import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlSeeAlso({Bird.class, Cat.class, Dog.class})
    @XmlDiscriminatorNode("@type")
    public abstract class Animal {
    
        private String name;
    
    }
    

    Bird

    import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
    
    @XmlDiscriminatorValue("bird")
    public class Bird extends Animal {
    
        private String wingSpan;
        private String preferredFood;
    
    }
    

    input.json/Output

    {
       "animals" : [ {
          "type" : "bird",
          "name" : "bird-1",
          "wingSpan" : "6 feets",
          "preferredFood" : "food-1"
       }, {
          "type" : "cat",
          "name" : "cat-1",
          "favoriteToy" : "toy-1"
       }, {
          "type" : "dog",
          "name" : "dog-1",
          "breed" : "bread-1",
          "leashColor" : "black"
       } ]
    }
    

    For More Information

    OPTION #2 - @XmlClassExtractor

    ClassExtractor (AnimalExtractor)

    You can write some code that will determine the appropriate subclass based on the JSON content.

    import org.eclipse.persistence.descriptors.ClassExtractor;
    import org.eclipse.persistence.sessions.*;
    
    public class AnimalExtractor extends ClassExtractor {
    
        @Override
        public Class extractClassFromRow(Record record, Session session) {
            if(null != record.get("@wingSpan") || null != record.get("@preferredFood")) {
                return Bird.class;
            } else if(null != record.get("@favoriteToy")) {
                return Cat.class;
            } else {
                return Dog.class;
            }
        }
    
    }
    

    Animal

    The @XmlClassExtractor annotation is used to specify the ClassExtractor.

    import javax.xml.bind.annotation.*;
    import org.eclipse.persistence.oxm.annotations.XmlClassExtractor;
    
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlSeeAlso({Bird.class, Cat.class, Dog.class})
    @XmlClassExtractor(AnimalExtractor.class)
    public abstract class Animal {
    
        private String name;
    
    }
    

    Bird

    Due to how MOXy processes the @XmlElement and @XmlAttribute annotations, any of the data you want to be made available to the ClassExtractor will need to be annotated with @XmlAttribute.

    import javax.xml.bind.annotation.XmlAttribute;
    
    public class Bird extends Animal {
    
        @XmlAttribute
        private String wingSpan;
    
        @XmlAttribute
        private String preferredFood;
    
    }
    

    input.json/Output

    {
       "animals" : [ {
          "wingSpan" : "6 feets",
          "preferredFood" : "food-1",
          "name" : "bird-1"
       }, {
          "favoriteToy" : "toy-1",
          "name" : "cat-1"
       }, {
          "breed" : "bread-1",
          "leashColor" : "black",
          "name" : "dog-1"
       } ]
    }
    

    For More Information


    DEMO CODE

    The following demo code can be used with both of the mappings described above.

    import java.util.*;
    import javax.xml.bind.*;
    import javax.xml.transform.stream.StreamSource;
    import org.eclipse.persistence.jaxb.JAXBContextProperties;
    
    public class Demo {
    
        public static void main(String[] args) throws Exception {
            Map<String, Object> properties = new HashMap<String, Object>();
            properties.put(JAXBContextProperties.MEDIA_TYPE, "application/json");
            properties.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
            JAXBContext jc = JAXBContext.newInstance(new Class[] {Zoo.class}, properties);
    
            Unmarshaller unmarshaller = jc.createUnmarshaller();
            StreamSource json = new StreamSource("src/forum14210676/input.json");
            Zoo zoo = unmarshaller.unmarshal(json, Zoo.class).getValue();
    
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.marshal(zoo, System.out);
        }
    
    }