Benjamin M
Benjamin M

Reputation: 24527

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

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==

Upvotes: 5

Views: 4085

Answers (3)

sapisch
sapisch

Reputation: 555

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.

Upvotes: 3

GolamMazid Sajib
GolamMazid Sajib

Reputation: 9447

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;
    }
}

Upvotes: 0

Bohdan Petrenko
Bohdan Petrenko

Reputation: 1155

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.

Upvotes: 1

Related Questions