2

I have a class like below:

@JsonRootName("ASSETS")
public class Assets{
    String val1;
    String val2;
}

Unfortunately I need to serialize it to something like this:

<ASSETS>
   <ASSET>
      <val1_val2>
         <val1>x</val1>
         <val2>y</val2>
      </val1_val2>
   </ASSET>
   <ASSET>
      <val1_val2>
         <val1>x</val1>
         <val2>y</val2>
      </val1_val2>
   </ASSET>
</ASSETS>

I can get it to the point where I have a list of ASSET objects inside ASSETS, but how do I add that extra wrapper that's a composite of two of the fields?

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
Steve
  • 4,457
  • 12
  • 48
  • 89

1 Answers1

2

You need to write custom serialiser. To do that, extend com.fasterxml.jackson.databind.JsonSerializer class. Also, to create extra wrap element use startWrappedValue method. Example code could look like below:

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class XmlMapperApp {

    public static void main(String[] args) throws Exception {
        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.enable(SerializationFeature.INDENT_OUTPUT);
        xmlMapper.setDefaultUseWrapper(false);

        Assets assets = new Assets();
        assets.getAssets().add(new Asset());
        assets.getAssets().add(new Asset());

        System.out.println(xmlMapper.writeValueAsString(assets));
    }
}

class AssetXMLSerializer extends JsonSerializer<Asset> {

    private final QName wrapper = new QName("val1_val2");
    @Override
    public void serialize(Asset value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        ToXmlGenerator xmlGen = (ToXmlGenerator) gen;

        xmlGen.writeStartObject();
        xmlGen.startWrappedValue(wrapper, wrapper);
        xmlGen.writeStringField("val1", value.getVal1());
        xmlGen.writeStringField("val2", value.getVal2());
        xmlGen.finishWrappedValue(wrapper, wrapper);
        xmlGen.writeEndObject();

    }
}

@JsonRootName("ASSETS")
class Assets {

    @JacksonXmlProperty(localName = "ASSET")
    private List<Asset> assets = new ArrayList<>();

    // getters, setters, toString
}

@JsonSerialize(using = AssetXMLSerializer.class)
class Asset {
    private String val1;
    private String val2;

    // getters, setters, toString
}

Above code prints:

<ASSETS>
  <ASSET>
    <val1_val2>
      <val1>x</val1>
      <val2>y</val2>
    </val1_val2>
  </ASSET>
  <ASSET>
    <val1_val2>
      <val1>x</val1>
      <val2>y</val2>
    </val1_val2>
  </ASSET>
</ASSETS>

Solution with using default serialiser for a huge bean

To use default bean serialiser for an Asset class you need to use BeanSerializerModifier and register it using SimpleModule. We need to invoke serializeFields method which is protected, so I created ExpandXmlBeanSerializer just to make it public so we can use it in our implementation:

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.BeanSerializerBase;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import com.fasterxml.jackson.dataformat.xml.ser.XmlBeanSerializer;

import javax.xml.namespace.QName;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class XmlMapperApp {

    public static void main(String[] args) throws Exception {
        SimpleModule assetModule = new SimpleModule();
        assetModule.setSerializerModifier(new LoopBackBeanSerializerModifier());

        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.registerModule(assetModule);
        xmlMapper.enable(SerializationFeature.INDENT_OUTPUT);
        xmlMapper.setDefaultUseWrapper(false);

        Assets assets = new Assets();
        assets.getAssets().add(new Asset("x0", "y0"));
        assets.getAssets().add(new Asset("x1", "y1"));

        System.out.println(xmlMapper.writeValueAsString(assets));
    }
}

class LoopBackBeanSerializerModifier extends BeanSerializerModifier {
    @Override
    public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
        if (beanDesc.getBeanClass() == Asset.class) {
            return new AssetXMLSerializer(new ExpandXmlBeanSerializer((BeanSerializerBase) serializer));
        }
        return serializer;
    }
}

class ExpandXmlBeanSerializer extends XmlBeanSerializer {

    public ExpandXmlBeanSerializer(BeanSerializerBase src) {
        super(src);
    }

    @Override
    public void serializeFields(Object bean, JsonGenerator gen0, SerializerProvider provider) throws IOException {
        super.serializeFields(bean, gen0, provider);
    }
}

class AssetXMLSerializer extends JsonSerializer<Asset> {

    private final QName wrapper;
    private final ExpandXmlBeanSerializer baseSerializer;

    public AssetXMLSerializer(ExpandXmlBeanSerializer baseSerializer) {
        this.baseSerializer = Objects.requireNonNull(baseSerializer);
        String fields = String.join("_",
                Stream.of(Asset.class.getDeclaredFields())
                .map(Field::getName)
                .collect(Collectors.toList()));
        this.wrapper = new QName(fields);
    }

    @Override
    public void serialize(Asset value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        ToXmlGenerator xmlGen = (ToXmlGenerator) gen;

        xmlGen.writeStartObject();
        xmlGen.startWrappedValue(wrapper, wrapper);
        baseSerializer.serializeFields(value, gen, serializers);
        xmlGen.finishWrappedValue(wrapper, wrapper);
        xmlGen.writeEndObject();

    }
}

@JsonRootName("ASSETS")
class Assets {

    @JacksonXmlProperty(localName = "ASSET")
    private List<Asset> assets = new ArrayList<>();

    // getters, setters, toString
}

class Asset {
    private String val1;
    private String val2;

    public Asset(String val1, String val2) {
        this.val1 = val1;
        this.val2 = val2;
    }

    // getters, setters, toString
}

See also:

  1. Jackson xml and json root element
  2. Using Jackson to add XML attributes to manually-built node-tree
  3. Multiple Jackson XML Custom (XMLStreamWriter) Serialisers throws Exception
  4. SpringBoot: Consume & Produce XML with a Custom Serializer + Deserializer

  5. Jackson deserialization SNS message error MismatchedInputException

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
  • This works! Any tip to handle very large numbers of fields? (instead of manually putting in var1, var2, etc) – Steve Oct 07 '19 at 19:15
  • While I don't love that the solution uses reflection, I really appreciate that you dug deep on this one for me, thank you so much! This saved my butt. – Steve Oct 07 '19 at 20:40
  • 1
    @Steve, reflection is used only to generate wrapper name. You can do that in other way if reflection is too "strong" for this job. – Michał Ziober Oct 07 '19 at 20:44
  • 1
    As I'm reading it more closely, I can see that. Thanks again, this is a great solution! – Steve Oct 07 '19 at 20:46