FasterXML / jackson-dataformats-binary

Uber-project for standard Jackson binary format backends: avro, cbor, ion, protobuf, smile
Apache License 2.0
310 stars 133 forks source link

[avro] Unable to encode and decodea POJO with an auto-generated schema #309

Open lburgazzoli opened 2 years ago

lburgazzoli commented 2 years ago

I wrote a small example that is about encoding and decoding a POJO with an auto generated schema, the code is like:

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.slf4j:slf4j-simple:1.7.30
//DEPS com.fasterxml.jackson.core:jackson-core:2.12.5
//DEPS com.fasterxml.jackson.core:jackson-databind:2.12.5
//DEPS com.fasterxml.jackson.dataformat:jackson-dataformat-avro:2.12.5
//DEPS com.slack.api:slack-api-model:1.8.1

import java.util.List;
import java.util.UUID;

import com.fasterxml.jackson.dataformat.avro.AvroMapper;
import com.slack.api.model.Message;
import com.slack.api.model.block.SectionBlock;
import com.slack.api.model.block.composition.MarkdownTextObject;

public class av {
    public static void main(String[] args) throws Exception {
        var message = new Message();
        message.setBlocks(List.of(SectionBlock
                .builder()
                .text(MarkdownTextObject
                        .builder()
                        .text("*Hello from Jackson!*")
                        .build())
                .build()));

        serdes(Message.class, message);
    }

    public static <T> void serdes(Class<T> type, T instance) throws Exception {
        var mapper = new AvroMapper();
        var schema = mapper.schemaFor(type);

        var raw = mapper.writer().with(schema).writeValueAsBytes(instance);
        var obj = mapper.readerFor(type).with(schema).readValue(raw);

        System.out.println("> " + raw);
        System.out.println("> " + obj);
    }
}

When running the code, then an exception is thrown:

xception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: No field named 'accessory' (through reference chain: com.slack.api.model.Message["blocks"]->java.util.ImmutableCollections$List12[0]->com.slack.api.model.block.SectionBlock["accessory"])
        at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:390)
        at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:349)
        at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:316)
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:778)
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
        at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:728)
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)
        at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:178)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)
        at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1514)
        at com.fasterxml.jackson.databind.ObjectWriter._writeValueAndClose(ObjectWriter.java:1215)
        at com.fasterxml.jackson.databind.ObjectWriter.writeValueAsBytes(ObjectWriter.java:1108)
        at av.serdes(av.java:35)
        at av.main(av.java:28)
        Suppressed: com.fasterxml.jackson.core.JsonGenerationException: Failed to close AvroGenerator: (java.lang.NullPointerException): null of boolean in field displayAsBot of com.slack.api.model.Message
                at com.fasterxml.jackson.dataformat.avro.AvroGenerator.close(AvroGenerator.java:353)
                at com.fasterxml.jackson.databind.util.ClassUtil.closeOnFailAndThrowAsIOE(ClassUtil.java:497)
                at com.fasterxml.jackson.databind.ObjectWriter._writeValueAndClose(ObjectWriter.java:1217)
                ... 3 more
        Caused by: java.lang.NullPointerException: null of boolean in field displayAsBot of com.slack.api.model.Message
                at org.apache.avro.generic.GenericDatumWriter.npe(GenericDatumWriter.java:145)
                at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:139)
                at com.fasterxml.jackson.dataformat.avro.ser.NonBSGenericDatumWriter.write(NonBSGenericDatumWriter.java:123)
                at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:62)
                at com.fasterxml.jackson.dataformat.avro.ser.RootContext.complete(RootContext.java:122)
                at com.fasterxml.jackson.dataformat.avro.AvroGenerator._complete(AvroGenerator.java:627)
                at com.fasterxml.jackson.dataformat.avro.AvroGenerator.close(AvroGenerator.java:348)
                ... 5 more
        Caused by: java.lang.NullPointerException
                at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:134)
                at com.fasterxml.jackson.dataformat.avro.ser.NonBSGenericDatumWriter.write(NonBSGenericDatumWriter.java:43)
                at org.apache.avro.generic.GenericDatumWriter.writeField(GenericDatumWriter.java:166)
                at org.apache.avro.generic.GenericDatumWriter.writeRecord(GenericDatumWriter.java:156)
                at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:118)
                ... 10 more
Caused by: java.lang.IllegalStateException: No field named 'accessory'
        at com.fasterxml.jackson.dataformat.avro.ser.ObjectWriteContext._reportUnknownField(ObjectWriteContext.java:136)
        at com.fasterxml.jackson.dataformat.avro.ser.ObjectWriteContext.writeFieldName(ObjectWriteContext.java:65)
        at com.fasterxml.jackson.dataformat.avro.AvroGenerator.writeFieldName(AvroGenerator.java:307)
        at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:694)
        at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:770)
        ... 14 more

I'm not sure if this comes from how the Message class is implemented (unfortunately it is outside of my control) or about eh way I'm using jackson.

Any help would be appreciated.

cowtowncoder commented 2 years ago

Looks like it should work, but to know for sure you would need to include definition of type Message. It would probably be good to first debug with JSON writer/reader, see if there is an issue with POJO definition itself wrt round-trip handling.

lburgazzoli commented 2 years ago

The message definition is here: https://github.com/slackapi/java-slack-sdk/blob/main/slack-api-model/src/main/java/com/slack/api/model/Message.java

I bet the issue is with the gson annotation that are being used

lburgazzoli commented 2 years ago

Doing the same thing with plain json:

    public static <T> void json(Class<T> type, T instance) throws Exception {
        var mapper = new ObjectMapper();

        var raw = mapper.writer().writeValueAsBytes(instance);
        var obj = mapper.readerFor(type).readValue(raw);

        System.out.println("> " + raw);
        System.out.println("> " + obj);
    }

Leads to:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.slack.api.model.block.LayoutBlock` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (byte[])"{"type":null,"subtype":null,"team":null,"channel":null,"user":null,"username":null,"text":null,"blocks":[{"type":"section","text":{"type":"mrkdwn","text":"*Hello from Jackson!*","verbatim":null},"blockId":null,"fields":null,"accessory":null}],"attachments":null,"ts":null,"threadTs":null,"intro":false,"starred":false,"wibblr":false,"pinnedTo":null,"reactions":null,"botId":null,"botLink":null,"displayAsBot":false,"botProfile":null,"icons":null,"file":null,"files":null,"upload":false,"parentUserId""[truncated 373 bytes]; line: 1, column: 106] (through reference chain: com.slack.api.model.Message["blocks"]->java.util.ArrayList[0])
        at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
        at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764)
        at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
        at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1209)
        at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:355)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
        at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
        at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
        at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
        at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
        at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2033)
        at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1528)
        at av.json(av.java:37)
        at av.main(av.java:29)
cowtowncoder commented 2 years ago

Exception claims that LayoutBlock is abstract type: if so, there has to be something to tell which actual concrete type is to be constructed. This can be done by:

  1. Adding explicit mapping (either on mapper or using annotations like @JsonDeserialize(as = ConcreteType.class))
  2. Enabling polymorphic type handling (this requires addition of Type Id during serialization)

Gson annotations would not be used but you would likely need their equivalent for Jackson if default names are changed (which seems to be the case).