3

I am trying to serialise and deserialise a class RuleMessage but can't get it to work. Here is my code:

public class RuleMessage {
    private String id;
    private SerializableRunnable sRunnable;

    public RuleMessage(String id, SerializableRunnable sRunnable) {
        this.id = id;
        this.sRunnable = sRunnable;
    }
}
public interface SerializableRunnable extends Runnable, Serializable {
}
    @Test
    public void testSerialization() throws JsonProcessingException {
        MAPPER.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, 
                                                               JsonTypeInfo.As.PROPERTY);
        
        SerializableRunnable r = () -> System.out.println("Serializable!");

        RuleMessage rule = new RuleMessage("1", r);
        System.out.println(MAPPER.writeValueAsString(businessRule));
}

I am using Java 8. Can someone tell me if this is possible in the Jackson library?

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
Prince
  • 145
  • 1
  • 7

2 Answers2

3

Jackson was created to keep object state not behaviour. This is why it tries to serialise POJO's properties using getters, setters, etc. Serialising lambdas break this idea. Theres is no any property to serialise, only a method which should be invoked. Serialising raw lambda object is really bad idea and you should redesign your app to avoid uses cases like this.

In your case SerializableRunnable interface extends java.io.Serializable which gives one option - Java Serialisation. Using java.io.ObjectOutputStream we can serialise lambda object to byte array and serialise it in JSON payload using Base64 encoding. Jackson supports this scenario providing writeBinary and getBinaryValue methods.

Simple example could look like below:

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class JsonLambdaApp {
    public static void main(String[] args) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);

        SerializableRunnable action = () -> System.out.println("Serializable!");

        String json = mapper.writeValueAsString(new RuleMessage("1", action));
        System.out.println(json);

        RuleMessage ruleMessage = mapper.readValue(json, RuleMessage.class);
        ruleMessage.getsRunnable().run();
    }
}

@JsonSerialize(using = LambdaJsonSerializer.class)
@JsonDeserialize(using = LambdaJsonDeserializer.class)
interface SerializableRunnable extends Runnable, Serializable {
}

class LambdaJsonSerializer extends JsonSerializer<SerializableRunnable> {

    @Override
    public void serialize(SerializableRunnable value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             ObjectOutputStream outputStream = new ObjectOutputStream(byteArrayOutputStream)) {
            outputStream.writeObject(value);
            gen.writeBinary(byteArrayOutputStream.toByteArray());
        }
    }
}

class LambdaJsonDeserializer extends JsonDeserializer<SerializableRunnable> {
    @Override
    public SerializableRunnable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        byte[] value = p.getBinaryValue();
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(value);
             ObjectInputStream inputStream = new ObjectInputStream(byteArrayInputStream)) {
            return (SerializableRunnable) inputStream.readObject();
        } catch (ClassNotFoundException e) {
            throw new IOException(e);
        }
    }
}

class RuleMessage {
    private String id;
    private SerializableRunnable sRunnable;

    @JsonCreator
    public RuleMessage(@JsonProperty("id") String id, @JsonProperty("sRunnable") SerializableRunnable sRunnable) {
        this.id = id;
        this.sRunnable = sRunnable;
    }

    public String getId() {
        return id;
    }

    public SerializableRunnable getsRunnable() {
        return sRunnable;
    }
}

Above code prints JSON:

{
  "id" : "1",
  "sRunnable" : "rO0ABXNyACFqYXZhLmxhbmcuaW52b2tlLlNlcmlhbGl6ZWRMYW1iZGFvYdCULCk2hQIACkkADmltcGxNZXRob2RLaW5kWwAMY2FwdHVyZWRBcmdzdAATW0xqYXZhL2xhbmcvT2JqZWN0O0wADmNhcHR1cmluZ0NsYXNzdAARTGphdmEvbGFuZy9DbGFzcztMABhmdW5jdGlvbmFsSW50ZXJmYWNlQ2xhc3N0ABJMamF2YS9sYW5nL1N0cmluZztMAB1mdW5jdGlvbmFsSW50ZXJmYWNlTWV0aG9kTmFtZXEAfgADTAAiZnVuY3Rpb25hbEludGVyZmFjZU1ldGhvZFNpZ25hdHVyZXEAfgADTAAJaW1wbENsYXNzcQB+AANMAA5pbXBsTWV0aG9kTmFtZXEAfgADTAATaW1wbE1ldGhvZFNpZ25hdHVyZXEAfgADTAAWaW5zdGFudGlhdGVkTWV0aG9kVHlwZXEAfgADeHAAAAAGdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAHZyABxjb20uY2Vsb3hpdHkuSnNvblR5cGVJbmZvQXBwAAAAAAAAAAAAAAB4cHQAIWNvbS9jZWxveGl0eS9TZXJpYWxpemFibGVSdW5uYWJsZXQAA3J1bnQAAygpVnQAHGNvbS9jZWxveGl0eS9Kc29uVHlwZUluZm9BcHB0ABZsYW1iZGEkbWFpbiQ1YzRiNmEwOCQxcQB+AAtxAH4ACw=="
}

and lambda:

Serializable!

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146
0

First, in RuleMessage you have to either create getters / setters or make the fields public in order to provide Jackson access to the fields.

Your code then prints something like this:

{"@class":"RuleMessage","id":"1","sRunnable":{"@class":"RuleMessage$$Lambda$20/0x0000000800b91c40"}}

This JSON document cannot be deserialized because RuleMessage has no default constructor and the lambda cannot be constructed.

Instead of the lambda, you could create a class:

  public class Runner implements SerializableRunnable {
    @Override
    public void run() {
      System.out.println("Serializable!");
    }
  }

and construct your pojo like this:

new RuleMessage("1", new Runner())

The Jackson deserializer is now able to reconstruct the objects and execute the runner.

aeberhart
  • 744
  • 1
  • 4
  • 15
  • Thanks for your reply. I do have a default constructor and getters/setters, I removed them for brevity(should have mentioned that). I want to have a flexible design where I can wrap a method call in a lambda function and serialize it into the db. At a later time, I would like to deserialize the lambda and invoke it. I tried implementing the class within the function but Jackson cannot serialize that. It works fine if I move the class outside the function. Is there a way to serialize inner classes or lambda in Jackson? – Prince Sep 06 '20 at 18:54