Jackson serialize Object to JSON to base64 (without endless loop)

Viewed 1713

Is there a simple way to serialize an object using Jackson to base64 encoded JSON? (object -> JSON -> base64)

I tried using a custom StdSerializer, but this (of course) results in a endless loop:

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, value);
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

A workaround is to copy all fields to another class and use that class for the intermediate representation:

class TmpFoo {
  public String field1;
  public int field2;
  // ...
}

class MySerializer extends StdSerializer<Foo> {
  public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) {
    TmpFoo tmp = new TmpFoo();
    tmp.field1 = value.field1;
    tmp.field2 = value.field2;
    // etc.

    StringWriter stringWriter = new StringWriter();
    JsonGenerator newGen = gen.getCodec().getFactory().createGenerator(stringWriter);
    gen.getCodec().getFactory().getCodec().writeValue(newGen, tmp); // here "tmp" instead of "value"
    String json = stringWriter.toString();
    String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
    gen.writeString(base64);
  }
}

Creating a new ObjectMapper is not desired, because I need all registered modules and serializers of the default ObjectMapper.

I was hoping for some easier way of achieving this.


EDIT: Example

Step 1: Java Object

class Foo {
  String field1 = "foo";
  int field2 = 42;
}

Step 2: JSON

{"field1":"foo","field2":42}

Step 3: Base64

eyJmaWVsZDEiOiJmb28iLCJmaWVsZDIiOjQyfQ==
3 Answers

According to this site, there is a workaround to avoid this recursion problem:

When we define a custom serializer, Jackson internally overrides the original BeanSerializer instance [...] our SerializerProvider finds the customized serializer every time, instead of the default one, and this causes an infinite loop.

A possible workaround is using BeanSerializerModifier to store the default serializer for the type Folder before Jackson internally overrides it.

If I understood the workaround correctly, your Serializer should look like this:

class FooSerializer extends StdSerializer<Foo> {

    private final JsonSerializer<Object> defaultSerializer;

    public FooSerializer(JsonSerializer<Object> defaultSerializer) {
        super(Foo.class);
        this.defaultSerializer = defaultSerializer;
    }

    @Override
    public void serialize(Foo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        StringWriter stringWriter = new StringWriter();
        JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
        defaultSerializer.serialize(value, tempGen, provider);

        tempGen.flush();

        String json = stringWriter.toString();
        String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
        gen.writeString(base64);
    }
}

In addition to the serializer, a modifier is needed:

public class FooBeanSerializerModifier extends BeanSerializerModifier {

    @Override
    public JsonSerializer<?> modifySerializer(
      SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {

        if (beanDesc.getBeanClass().equals(Foo.class)) {
            return new FooSerializer((JsonSerializer<Object>) serializer);
        }
        return serializer;
    }
}

Example module:

ObjectMapper mapper = new ObjectMapper();

SimpleModule module = new SimpleModule();
module.setSerializerModifier(new FooBeanSerializerModifier());

mapper.registerModule(module);

EDIT:

I've added flush() to flush the JsonGenerator tempGen. Also, I've created a minimal test enviroment with JUnit, which verifies your Example with Foo: The github repo can be found here.


EDIT: Alternative 2

Another (simple) option is using a wrapper class with generics:

public class Base64Wrapper<T> {

    private final T wrapped;

    private Base64Wrapper(T wrapped) {
        this.wrapped = wrapped;
    }

    public T getWrapped() {
        return this.wrapped;
    }

    public static <T> Base64Wrapper<T> of(T wrapped) {
        return new Base64Wrapper<>(wrapped);
    }
}
public class Base64WrapperSerializer extends StdSerializer<Base64Wrapper> {


    public Base64WrapperSerializer() {
        super(Base64Wrapper.class);
    }

    @Override
    public void serialize(Base64Wrapper value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        StringWriter stringWriter = new StringWriter();
        JsonGenerator tempGen = provider.getGenerator().getCodec().getFactory().createGenerator(stringWriter);
        provider.defaultSerializeValue(value.getWrapped(), tempGen);
        tempGen.flush();

        String json = stringWriter.toString();
        String base64 = new String(Base64.getEncoder().encode(json.getBytes()));
        gen.writeString(base64);
    }
}

An example usecase would be:

final ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer(new Base64WrapperSerializer());
mapper.registerModule(module);

final Foo foo = new Foo();
final Base64Wrapper<Foo> base64Wrapper = Base64Wrapper.of(foo);
final String base64Json = mapper.writeValueAsString(base64Wrapper);

This example can be found in this GitHub (branch: wrapper) repo, verifing you BASE64 String from your foo example with JUnit testing.

Instead of creating new object you may convert existing one into map. Like in the example below

import static java.nio.charset.StandardCharsets.UTF_8;

public class FooSerializer extends StdSerializer<Foo> {

    public FooSerializer() {
        super(Foo.class);
    }

    @Override
    public void serialize(Foo foo, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) {
        try {
            ObjectMapper mapper = (ObjectMapper) jsonGenerator.getCodec();
            var map = toMap(foo); // if you need class info for deserialization than use toMapWithClassInfo
            String json = mapper.writeValueAsString(map);
            jsonGenerator.writeString(Base64.getEncoder().encodeToString(json.getBytes(UTF_8)));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static Map<String, Object> toMap(Object o) throws Exception {
        Map<String, Object> result = new HashMap<>();
        Field[] declaredFields = o.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            result.put(field.getName(), field.get(o));
        }
        return result;
    }


    public static Map<String, Object> toMapWithClassInfo(Object obj) throws Exception {
        Map<String, Object> result = new HashMap<>();
        BeanInfo info = Introspector.getBeanInfo(obj.getClass());
        for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
            Method reader = pd.getReadMethod();
            if (reader != null)
                result.put(pd.getName(), reader.invoke(obj));
        }
        return result;
    }
}

I'm providing 2 ways of converting into map: with and without class info. Choose the one, applicable to your problem.

To serialize object jackson search @JsonValue method. You can add encodedJsonString method annotated by @JsonValue in Foo class.

Try with this:

@Getter
@Setter
public class Foo implements Serializable {

    private static final long serialVersionUID = 1L;

    public String field1;
    public int field2;

    @JsonValue
    public String toEncodedJsonString() {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            new ObjectOutputStream(baos).writeObject(this);
            return org.apache.commons.codec.binary.Base64.encodeBase64String(baos.toByteArray());
        }catch (Exception ex){

        }
        return null;
    }
}
Related