Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson Serialization: Setting field value as XML element name

We are using Jackson jax-rs XML content providers for handling XML content type in our jax-rs based REST API project. In the serializing a List of POJOs, we need to set the xml element name dynamically from a field in the POJO.

public class ResponsePOJO {
     @JacksonXmlProperty
     @JacksonXmlElementWrapper(useWrapping = false)
     private List<Message> message = new ArrayList<Message>();
}

public class Message {
     private String type; // "Error" or "Warning"
     private String msg; // The actual message
}

Default Jackson serialized XML:

<ResponsePOJO>
    <message>
        <type>Error</type>
        <msg>Some random error message</msg>
    </message>
    <message>
        <type>Warning</type>
        <msg>Some random warning message</msg>
    </message>
</ResponsePOJO>

Our requirement, ie., set type as the XML element name.

<ResponsePOJO>
    <Error>
        <msg>Some random error message</msg>
    </Error>
    <Warning>
        <msg>Some random warning message</msg>
    </Warning>
</ResponsePOJO>

In order to achieve this, we wrote a custom XML serializer in the following manner:

public class MessageListSerializer extends
        JsonSerializer<List<Message>> {
    @Override
    public void serialize(List<Message> value, JsonGenerator jgen,
            SerializerProvider provider) throws IOException,
            JsonProcessingException {

        for(Message me : value){
            jgen.writeObjectField(me.getType(), me);
        }
    }
}

And added the serializer using annotation:

@JacksonXmlProperty
@JacksonXmlElementWrapper(useWrapping = false)
@JsonSerialize(using=MessageListSerializer.class)
private List<Message> message = new ArrayList<Message>();

But while serializing the ResponsePOJO using Jackson XMLMapper, we are getting the following exception...

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Array index out of range: -2
    at com.fasterxml.jackson.dataformat.xml.ser.XmlSerializerProvider.serializeValue(XmlSerializerProvider.java:100)
    at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:2866)
    at com.fasterxml.jackson.databind.ObjectMapper.writeValue(ObjectMapper.java:2289)
Caused by: java.lang.ArrayIndexOutOfBoundsException: Array index out of range: -2
    at com.ctc.wstx.sw.BufferingXmlWriter.writeRaw(BufferingXmlWriter.java:241)
    at com.ctc.wstx.sw.BaseStreamWriter.writeRaw(BaseStreamWriter.java:1113)
    at com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator.writeRaw(ToXmlGenerator.java:592)
    at com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter$Lf2SpacesIndenter.writeIndentation(DefaultXmlPrettyPrinter.java:517)
    at com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter.writeEndObject(DefaultXmlPrettyPrinter.java:223)
    at com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator.writeEndObject(ToXmlGenerator.java:422)
    at com.fasterxml.jackson.dataformat.xml.ser.XmlBeanSerializer.serialize(XmlBeanSerializer.java:119)
    at com.fasterxml.jackson.dataformat.xml.ser.XmlSerializerProvider.serializeValue(XmlSerializerProvider.java:92)
    ... 3 more

Could you please help me resolve this issue...

like image 902
dinup24 Avatar asked Apr 08 '15 13:04

dinup24


People also ask

Can Jackson parse XML?

Jackson is a library for handling JSON in Java systems and now has support for XML from version 2. DOM4J is a memory-efficient library for parsing XML, XPath, and XSLT (eXtensible Stylesheet Language).

Can ObjectMapper be used for XML?

Reading XMLWe can also read XML, using the various readValue APIs that are part of provided by the ObjectMapper. For example, reading some XML from an InputStream into a Java Bean: MyBean bean = objectMapper.

What is Jackson Dataformat XML?

Package. Description. com.fasterxml.jackson.dataformat.xml. Package that contains XML-based backends which can serialize POJOs to and deserialize from XML, using Stax XML parsers and generators for XML processing and mostly standard Jackson data binding otherwise.


3 Answers

Edit to previous solution: You're nearly there, just need to add @JsonIgnore to private String type; // "Error" or "Warning"

<ResponsePOJO>
    <Error>
        <msg>error message</msg>
    </Error>
    <Warning>
        <msg>warning message</msg>
    </Warning>
</ResponsePOJO>

The following will output the above xml:

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        Main demo = new Main();
        demo.run();
    }


    public void run(){

        ObjectMapper xmlMapper = new XmlMapper();

        ResponsePOJO responsePOJO = new ResponsePOJO();

        Message message = new Message();
        message.setType("Error");
        message.setMsg("error message");
        Message message2 = new Message();
        message2.setType("Warning");
        message2.setMsg("warning message");

        responsePOJO.getMessage().add(message);
        responsePOJO.getMessage().add(message2);
        try {
            String xml = xmlMapper.writeValueAsString(responsePOJO);
            System.out.println(xml);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

    }


    public class ResponsePOJO {
        @JacksonXmlProperty
        @JacksonXmlElementWrapper(useWrapping = false)
        @JsonSerialize(using=MessageListSerializer.class)
        private List<Message> message = new ArrayList<Message>();

        public List<Message> getMessage() {
            return message;
        }

    }


    public class Message {
        @JsonIgnore
        private String type; // "Error" or "Warning"
        private String msg; // The actual message

        public String getType() {
            return type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }
    }


}

along with the class

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.util.List;

/**
 * Created by Pand on 08/04/2015.
 */
public class MessageListSerializer extends
        JsonSerializer<List<Main.Message>> {


    @Override
    public void serialize(List<Main.Message> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {

        for(Main.Message me : value){
            jgen.writeObjectField(me.getType(), me);
        }
    }


}

with dependencies

    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.woodstox</groupId>
            <artifactId>woodstox-core-asl</artifactId>
            <version>4.1.4</version>
        </dependency>
    </dependencies>
like image 140
pandemicGander Avatar answered Oct 22 '22 10:10

pandemicGander


"I cannot post it in comment since it is too long" The following is the customized classes:

 @XmlRootElement
 @XmlAccessorType(XmlAccessType.FIELD)
 public class MyResponse {

 @XmlElements({ @XmlElement(name = "error", type = MyError.class),
  @XmlElement(name = "warning", type = MyWarning.class) })
 @XmlElementWrapper
  private List<MyMessage> messages = Lists.newArrayList();

  public List<MyMessage> getMessages() {
  return messages;
 }

 public void setMessages(List<MyMessage> messages) {
  this.messages = messages;
 }
}    

@XmlAccessorType(XmlAccessType.FIELD)
public class MyMessage {
 protected String text;

 public String getText() {
  return text;
  }

  public void setText(String text) {
  this.text = text;
  }
  }

@XmlAccessorType(XmlAccessType.FIELD)
  public class MyError extends MyMessage {
 }

@XmlAccessorType(XmlAccessType.FIELD)
public class MyWarning extends MyMessage {

} 

I tested it with my demo code:

MyResponse myResponse = new MyResponse();
MyMessage error = new MyError();
error.setText("error");

MyMessage warning = new MyWarning();
warning.setText("warning");
myResponse.setMessages(Lists.newArrayList(error, warning));

and it returned:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<myResponse> 
  <messages>
    <error>
      <text>error</text>
    </error>
    <warning>
      <text>warning</text>
    </warning>
  </messages>
</myResponse> 

You need to tweak element name to get desired results though.

like image 37
ABOS Avatar answered Oct 22 '22 10:10

ABOS


Similar problem to me. I wrote a custom JsonSerializer to generate different xml element name (read from @JsonTypeName) for each item in a collection.

Here is my JsonSerializer:

import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

public class NamedCollectionXmlSerializer extends JsonSerializer<Collection<Object>> {
    @Override
    public void serialize(Collection<Object> list, JsonGenerator gen, SerializerProvider provider) throws IOException {
        boolean toXml = gen instanceof ToXmlGenerator;
        if (!toXml) {
            // fallback to the default behavior for non-xml serialization
            gen.writeObject(list);
            return;
        }
        gen.writeStartArray();
        if (list != null) {
            for (Object item : list) {
                if (item == null) {
                    continue;
                }
                JsonTypeName jsonTypeName;
                if ((jsonTypeName = item.getClass().getAnnotation(JsonTypeName.class)) != null) {
                    // read JsonTypeName as the xml element name
                    // if JsonTypeName not present, use the default name
                    ((ToXmlGenerator) gen).setNextName(new QName("", jsonTypeName.value()));
                }
                gen.writeObject(item);
            }
        }
        gen.writeEndArray();
    }
}

for the following POJO (getters and constructors generated by lombok):

interface Message {

}

@Getter
@RequiredArgsConstructor
class ResponsePOJO {
    @JacksonXmlElementWrapper(useWrapping = false)
    @JsonSerialize(using = NamedCollectionXmlSerializer.class)
    private final List<Message> messages;
}

@Getter
@RequiredArgsConstructor
@JsonTypeName("Error")
class Error implements Message {
    private final String msg;
}

@Getter
@RequiredArgsConstructor
@JsonTypeName("Warning")
class Warning implements Message {
    private final String msg;

}

and test code:

ResponsePOJO response = new ResponsePOJO(
        Arrays.asList(new Error("error1"), new Warning("warn1"), new Error("error2"))
);
new XmlMapper().writerWithDefaultPrettyPrinter().writeValue(System.out, response);

this is output:

<ResponsePOJO>
  <Error>
    <msg>error1</msg>
  </Error>
  <Warning>
    <msg>warn1</msg>
  </Warning>
  <Error>
    <msg>error2</msg>
  </Error>
</ResponsePOJO>

PS: I test my code with jackson version 2.9.3

like image 41
gongshw Avatar answered Oct 22 '22 10:10

gongshw