FasterXML / jackson-module-scala

Add-on module for Jackson (https://github.com/FasterXML/jackson) to support Scala-specific datatypes
Apache License 2.0
500 stars 141 forks source link

Custom serializer with type information and yaml output #192

Open migel opened 9 years ago

migel commented 9 years ago

Hi, I'm trying to write a custom serializer for an Nd4j array (see http://nd4j.org). I was about to get it working with json but when I try yaml output I get an exception:

  Cause: com.fasterxml.jackson.dataformat.yaml.snakeyaml.emitter.EmitterException: expected NodeEvent, but got <com.fasterxml.jackson.dataformat.yaml.snakeyaml.events.DocumentEndEvent()>
  at com.fasterxml.jackson.dataformat.yaml.snakeyaml.emitter.Emitter.expectNode(Emitter.java:409)
  at com.fasterxml.jackson.dataformat.yaml.snakeyaml.emitter.Emitter.access$1600(Emitter.java:63)
  at com.fasterxml.jackson.dataformat.yaml.snakeyaml.emitter.Emitter$ExpectBlockMappingValue.expect(Emitter.java:651)
  at com.fasterxml.jackson.dataformat.yaml.snakeyaml.emitter.Emitter.emit(Emitter.java:217)
  at com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.close(YAMLGenerator.java:308)
  at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:3398)
  at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:2779)

In order to simplify the test I created dummy classes for Nd4j so I know the issue is not specific to that package. This test case:

import com.fasterxml.jackson.databind.JsonSerializer
//import org.nd4j.linalg.api.ndarray.INDArray
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
//import org.nd4j.linalg.api.buffer.DataBuffer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.databind.jsontype.TypeSerializer
//import org.nd4j.linalg.factory.Nd4j
import com.fasterxml.jackson.annotation.JsonTypeInfo
import org.scalatest.FunSuite

// Dummy INDArray
class INDArray {
  val _data = new DataBuffer
  def shape() = Array[Int](1, 3)
  def data = _data
  def ordering() = 'f'
}

// Dummy DataBuffer
class DataBuffer {
  def dataType = 1
  def asBytes() = Array[Byte](1, 2, 3)
}

// Custom serializer for INDArray
class NDArraySerializer extends JsonSerializer[INDArray] {

  def serialize(
    value: INDArray,
    jgen: JsonGenerator,
    provider: SerializerProvider) {

    jgen.writeStartObject()

    jgen.writeArrayFieldStart("shape")
    for (i <- value.shape()) jgen.writeNumber(i)
    jgen.writeEndArray()

    val dtype =
      if (value.data.dataType == 1 /*DataBuffer.FLOAT*/ )
        "float"
      else
        "double"
    jgen.writeStringField("dtype", dtype)

    jgen.writeStringField("order", value.ordering().toString())

    jgen.writeBinaryField("data", value.data.asBytes())

    jgen.writeEndObject()
  }

  override def serializeWithType(
    value: INDArray,
    jgen: JsonGenerator,
    provider: SerializerProvider,
    typeSer: TypeSerializer) {

    typeSer.writeTypePrefixForObject(value, jgen)
    serialize(value, jgen, provider)
    typeSer.writeTypeSuffixForObject(value, jgen)
  }
}

class TestYAML extends FunSuite {

  test("yaml") {

    try {
      val nd4jModule = new SimpleModule()

      nd4jModule.addSerializer(classOf[INDArray], new NDArraySerializer())

      val jsonMapper = new ObjectMapper

      jsonMapper.registerModule(DefaultScalaModule)

      jsonMapper.registerModule(nd4jModule)

      jsonMapper.enableDefaultTyping(
        ObjectMapper.DefaultTyping.NON_FINAL,
        JsonTypeInfo.As.PROPERTY)

      val yamlMapper = new ObjectMapper(new YAMLFactory)

      yamlMapper.registerModule(DefaultScalaModule)

      yamlMapper.registerModule(nd4jModule)

      yamlMapper.enableDefaultTyping(
        ObjectMapper.DefaultTyping.NON_FINAL,
        JsonTypeInfo.As.PROPERTY)

      val array = new INDArray

      jsonMapper.writeValueAsString(array)

      yamlMapper.writeValueAsString(array)
    }
    catch {
      case e: Throwable => fail("shouldn't throw an exception.", e)
    }
  }
}

If I comment out the enableDefaultTyping call it works.

cowtowncoder commented 9 years ago

It sounds like this is probably not specific to Scala module, but rather something related to YAML module. If so, it would be great to have a Java equivalent of the test so that test could be aded to YAML module (which does not depend on Scala).

migel commented 9 years ago

java version:

import java.io.IOException;

import org.junit.Test;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

public class TestYAML {

    // Dummy INDArray
    static class INDArray {
        DataBuffer _data = new DataBuffer();

        public int[] shape() {
            return new int[] { 1, 3 };
        }

        public DataBuffer data() {
            return _data;
        }

        public char ordering() {
            return 'f';
        }
    }

    // Dummy DataBuffer
    static class DataBuffer {
        public int dataType() {
            return 1;
        }

        public byte[] asBytes() {
            return new byte[] { 1, 2, 3 };
        }
    }

    // Custom serializer for INDArray
    class NDArraySerializer extends JsonSerializer<INDArray> {

        public void serialize(INDArray value, JsonGenerator jgen,
                SerializerProvider provider) throws IOException {

            jgen.writeStartObject();

            jgen.writeArrayFieldStart("shape");
            for (int i : value.shape())
                jgen.writeNumber(i);
            jgen.writeEndArray();

            String dtype;
            if (value.data().dataType() == 1 /* DataBuffer.FLOAT */)
                dtype = "float";
            else
                dtype = "double";

            jgen.writeStringField("dtype", dtype);

            jgen.writeStringField("order", Character.toString(value.ordering()));

            jgen.writeBinaryField("data", value.data().asBytes());

            jgen.writeEndObject();
        }

        public void serializeWithType(INDArray value, JsonGenerator jgen,
                SerializerProvider provider, TypeSerializer typeSer)
                throws IOException {

            typeSer.writeTypePrefixForObject(value, jgen);
            serialize(value, jgen, provider);
            typeSer.writeTypeSuffixForObject(value, jgen);
        }
    }

    @Test
    public void test() throws JsonProcessingException {

        SimpleModule nd4jModule = new SimpleModule();
        nd4jModule.addSerializer(INDArray.class, new NDArraySerializer());

        ObjectMapper jsonMapper = new ObjectMapper();
        jsonMapper.registerModule(nd4jModule);
        jsonMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);

        ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
        yamlMapper.registerModule(nd4jModule);

        yamlMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);

        INDArray array = new INDArray();

        jsonMapper.writeValueAsString(array);
                // FAIL HERE:
        yamlMapper.writeValueAsString(array);
    }
}
migel commented 9 years ago

I realized that I've created an additional nesting when serializing with type information. I've changed it so it wouldn't and I don't get an exception in this case. I think the above case is still a bug. Changed code:

import java.io.IOException;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

public class TestYAML {

    // Dummy INDArray
    static class INDArray {
        DataBuffer _data = new DataBuffer();

        public int[] shape() {
            return new int[] { 1, 3 };
        }

        public DataBuffer data() {
            return _data;
        }

        public char ordering() {
            return 'f';
        }
    }

    // Dummy DataBuffer
    static class DataBuffer {
        public int dataType() {
            return 1;
        }

        public byte[] asBytes() {
            return new byte[] { 1, 2, 3 };
        }
    }

    // Custom serializer for INDArray
    class NDArraySerializer extends JsonSerializer<INDArray> {

        public void serialize(INDArray value, JsonGenerator jgen,
                SerializerProvider provider) throws IOException {

            jgen.writeStartObject();
            serializeFields(value, jgen);
            jgen.writeEndObject();
        }

        public void serializeWithType(INDArray value, JsonGenerator jgen,
                SerializerProvider provider, TypeSerializer typeSer)
                throws IOException {

            typeSer.writeTypePrefixForObject(value, jgen);
            // AVOID CREATING NESTED OBJECT
            serializeFields(value, jgen);
            typeSer.writeTypeSuffixForObject(value, jgen);
        }

        void serializeFields(INDArray value, JsonGenerator jgen)
                throws IOException {
            jgen.writeArrayFieldStart("shape");
            for (int i : value.shape())
                jgen.writeNumber(i);
            jgen.writeEndArray();

            String dtype;
            if (value.data().dataType() == 1 /* DataBuffer.FLOAT */)
                dtype = "float";
            else
                dtype = "double";

            jgen.writeStringField("dtype", dtype);

            jgen.writeStringField("order", Character.toString(value.ordering()));

            jgen.writeBinaryField("data", value.data().asBytes());
        }
    }

    @Test
    public void test() throws JsonProcessingException {

        SimpleModule nd4jModule = new SimpleModule();
        nd4jModule.addSerializer(INDArray.class, new NDArraySerializer());

        ObjectMapper jsonMapper = new ObjectMapper();
        jsonMapper.registerModule(nd4jModule);
        jsonMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);

        ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
        yamlMapper.registerModule(nd4jModule);

        yamlMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);

        INDArray array = new INDArray();

        jsonMapper.writeValueAsString(array);
        yamlMapper.writeValueAsString(array);
    }
}