From 575e4e24e7fa2ed4658d857b684d3f63b766a537 Mon Sep 17 00:00:00 2001 From: Bryan Harclerode Date: Wed, 1 Mar 2017 23:38:30 -0600 Subject: [PATCH 1/2] [Avro] Add support for @Union serialization --- .../avro/AvroAnnotationIntrospector.java | 16 +++ .../dataformat/avro/AvroGenerator.java | 33 +++-- .../avro/schema/AvroSchemaHelper.java | 10 +- .../dataformat/avro/schema/RecordVisitor.java | 16 +++ .../avro/ser/ArrayWriteContext.java | 9 +- .../dataformat/avro/ser/AvroWriteContext.java | 109 +++++++++++++--- .../avro/ser/NonBSGenericDatumWriter.java | 50 +------- .../avro/ser/ObjectWriteContext.java | 23 +++- .../avro/interop/annotations/UnionTest.java | 119 ++++++++++++++++++ 9 files changed, 302 insertions(+), 83 deletions(-) create mode 100644 avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java index 65f5c164b..0e8ecdf63 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java @@ -1,5 +1,6 @@ package com.fasterxml.jackson.dataformat.avro; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,6 +16,7 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; /** * Adds support for the following annotations from the Apache Avro implementation: * * * @since 2.9 @@ -107,4 +110,17 @@ public Object findSerializer(Annotated a) { } return null; } + + @Override + public List findSubtypes(Annotated a) { + Union union = _findAnnotation(a, Union.class); + if (union == null) { + return null; + } + ArrayList names = new ArrayList<>(union.value().length); + for (Class subtype : union.value()) { + names.add(new NamedType(subtype, AvroSchemaHelper.getTypeId(subtype))); + } + return names; + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroGenerator.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroGenerator.java index 00c320bc7..381de6a7d 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroGenerator.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroGenerator.java @@ -1,18 +1,26 @@ package com.fasterxml.jackson.dataformat.avro; -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.core.base.GeneratorBase; -import com.fasterxml.jackson.core.io.IOContext; -import com.fasterxml.jackson.dataformat.avro.ser.AvroWriteContext; - -import org.apache.avro.io.BinaryEncoder; - import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; +import org.apache.avro.io.BinaryEncoder; + +import com.fasterxml.jackson.core.Base64Variant; +import com.fasterxml.jackson.core.FormatFeature; +import com.fasterxml.jackson.core.FormatSchema; +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.PrettyPrinter; +import com.fasterxml.jackson.core.SerializableString; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.base.GeneratorBase; +import com.fasterxml.jackson.core.io.IOContext; +import com.fasterxml.jackson.dataformat.avro.ser.AvroWriteContext; + public class AvroGenerator extends GeneratorBase { /** @@ -381,6 +389,17 @@ public final void writeStartObject() throws IOException { _complete = false; } + @Override + public void writeStartObject(Object forValue) throws IOException { + _avroContext = _avroContext.createChildObjectContext(forValue); + _complete = false; + if(this._writeContext != null && forValue != null) { + this._writeContext.setCurrentValue(forValue); + } + + this.setCurrentValue(forValue); + } + @Override public final void writeEndObject() throws IOException { diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java index 800e98c92..005ae678c 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java @@ -192,6 +192,10 @@ protected static T throwUnsupported() { throw new UnsupportedOperationException("Format variation not supported"); } + public static String getTypeId(JavaType type) { + return getTypeId(type.getRawClass()); + } + /** * Initializes a record schema with metadata from the given class; this schema is returned in a non-finalized state, and still * needs to have fields added to it. @@ -227,11 +231,11 @@ public static Schema createEnumSchema(BeanDescription bean, List values) /** * Returns the Avro type ID for a given type */ - protected static String getTypeId(JavaType type) { + public static String getTypeId(Class type) { // Primitives use the name of the wrapper class as their type ID if (type.isPrimitive()) { - return ClassUtil.wrapperType(type.getRawClass()).getName(); + return ClassUtil.wrapperType(type).getName(); } - return type.getRawClass().getName(); + return type.getName(); } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java index ac4ff1cd5..4cd88a42b 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/RecordVisitor.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.jsontype.NamedType; import com.fasterxml.jackson.databind.ser.BeanPropertyWriter; import com.fasterxml.jackson.dataformat.avro.AvroFixedSize; @@ -44,10 +45,25 @@ public RecordVisitor(SerializerProvider p, JavaType type, DefinedSchemas schemas _schemas = schemas; // Check if the schema for this record is overridden BeanDescription bean = getProvider().getConfig().introspectDirectClassAnnotations(_type); + List subTypes = getProvider().getAnnotationIntrospector().findSubtypes(bean.getClassInfo()); AvroSchema ann = bean.getClassInfo().getAnnotation(AvroSchema.class); if (ann != null) { _avroSchema = AvroSchemaHelper.parseJsonSchema(ann.value()); _overridden = true; + } else if (subTypes != null && !subTypes.isEmpty()) { + List unionSchemas = new ArrayList<>(); + try { + for (NamedType subType : subTypes) { + JsonSerializer ser = getProvider().findValueSerializer(subType.getType()); + VisitorFormatWrapperImpl visitor = new VisitorFormatWrapperImpl(_schemas, getProvider()); + ser.acceptJsonFormatVisitor(visitor, getProvider().getTypeFactory().constructType(subType.getType())); + unionSchemas.add(visitor.getAvroSchema()); + } + _avroSchema = Schema.createUnion(unionSchemas); + _overridden = true; + } catch (JsonMappingException jme) { + throw new RuntimeException("Failed to build schema", jme); + } } else { _avroSchema = AvroSchemaHelper.initializeRecordSchema(bean); _overridden = false; diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ArrayWriteContext.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ArrayWriteContext.java index 6dd4f3064..b0126c595 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ArrayWriteContext.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ArrayWriteContext.java @@ -34,7 +34,14 @@ public final AvroWriteContext createChildObjectContext() throws JsonMappingExcep _array.add(child.rawValue()); return child; } - + + @Override + public AvroWriteContext createChildObjectContext(Object object) throws JsonMappingException { + AvroWriteContext child = _createObjectContext(_schema.getElementType(), object); + _array.add(child.rawValue()); + return child; + } + @Override public void writeValue(Object value) { _array.add(value); diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java index 0bca39cdd..f15561000 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java @@ -1,15 +1,21 @@ package com.fasterxml.jackson.dataformat.avro.ser; import java.io.IOException; +import java.math.BigDecimal; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; -import org.apache.avro.generic.*; +import org.apache.avro.UnresolvedUnionException; +import org.apache.avro.generic.GenericArray; +import org.apache.avro.generic.GenericData; +import org.apache.avro.generic.GenericRecord; import org.apache.avro.io.BinaryEncoder; +import org.apache.avro.reflect.ReflectData; import com.fasterxml.jackson.core.JsonStreamContext; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.dataformat.avro.AvroGenerator; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; public abstract class AvroWriteContext extends JsonStreamContext @@ -58,8 +64,9 @@ public void complete() throws IOException { throw new IllegalStateException("Can not be called on "+getClass().getName()); } - /* - /********************************************************** + + public AvroWriteContext createChildObjectContext(Object object) throws JsonMappingException { return createChildObjectContext(); } + /* Accessors /********************************************************** */ @@ -140,33 +147,36 @@ protected GenericRecord _createRecord(Schema schema) throws JsonMappingException throw new JsonMappingException(null, "Failed to create Record type from "+type, e); } } - + protected GenericArray _createArray(Schema schema) { if (schema.getType() == Schema.Type.UNION) { - Schema match = null; - for (Schema s : schema.getTypes()) { - if (s.getType() == Schema.Type.ARRAY) { - if (match != null) { - throw new IllegalStateException("Multiple Array types, can not figure out which to use for: " - +schema); - } - match = s; - } - } - if (match == null) { + int arraySchemaIndex = schema.getIndexNamed(Type.ARRAY.getName()); + if (arraySchemaIndex < 0) { throw new IllegalStateException("No Array type found in union type: "+schema); } - schema = match; + schema = schema.getTypes().get(arraySchemaIndex); } return new GenericData.Array(8, schema); } - protected AvroWriteContext _createObjectContext(Schema schema) throws JsonMappingException + protected AvroWriteContext _createObjectContext(Schema schema) throws JsonMappingException { + if (schema.getType() == Type.UNION) { + schema = _recordOrMapFromUnion(schema); + } + return _createObjectContext(schema, null); // Object doesn't matter as long as schema isn't a union + } + + protected AvroWriteContext _createObjectContext(Schema schema, Object object) throws JsonMappingException { Type type = schema.getType(); if (type == Schema.Type.UNION) { - schema = _recordOrMapFromUnion(schema); + try { + schema = resolveUnionSchema(schema, object); + } catch (UnresolvedUnionException e) { + // couldn't find an exact match + schema = _recordOrMapFromUnion(schema); + } type = schema.getType(); } if (type == Schema.Type.MAP) { @@ -194,6 +204,69 @@ protected Schema _recordOrMapFromUnion(Schema unionSchema) return match; } + /** + * Resolves the sub-schema from a union that should correspond to the {@code datum}. + * + * @param unionSchema Union of schemas from which to choose + * @param datum Object that needs to map to one of the schemas in {@code unionSchema} + * @return Index into {@link Schema#getTypes() unionSchema.getTypes()} that matches {@code datum} + * @see #resolveUnionSchema(Schema, Object) + * @throws org.apache.avro.UnresolvedUnionException if {@code unionSchema} does not have a schema that can encode {@code datum} + */ + public static int resolveUnionIndex(Schema unionSchema, Object datum) { + if (datum != null) { + int subOptimal = -1; + for(int i = 0, size = unionSchema.getTypes().size(); i < size; i++) { + Schema schema = unionSchema.getTypes().get(i); + if (datum instanceof BigDecimal) { + // BigDecimals can be shoved into a double, but optimally would be a String or byte[] with logical type information + if (schema.getType() == Type.DOUBLE) { + subOptimal = i; + continue; + } + } + if (datum instanceof String) { + // Jackson serializes enums as strings, so try and find a matching schema + if (schema.getType() == Type.ENUM && schema.hasEnumSymbol((String) datum)) { + return i; + } + // Jackson serializes char/Character as a string, so try and find a matching schema + if (schema.getType() == Type.INT + && ((String) datum).length() == 1 + && AvroSchemaHelper.getTypeId(Character.class).equals(schema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS)) + ) { + return i; + } + // Jackson serializes char[]/Character[] as a string, so try and find a matching schema + if (schema.getType() == Type.ARRAY + && schema.getElementType().getType() == Type.INT + && AvroSchemaHelper.getTypeId(Character.class).equals(schema.getElementType().getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS)) + ) { + return i; + } + } + } + // Did we find a sub-optimal match? + if (subOptimal != -1) { + return subOptimal; + } + } + return ReflectData.get().resolveUnion(unionSchema, datum); + } + + /** + * Resolves the sub-schema from a union that should correspond to the {@code datum}. + * + * @param unionSchema Union of schemas from which to choose + * @param datum Object that needs to map to one of the schemas in {@code unionSchema} + * @return Schema that matches {@code datum} + * @see #resolveUnionIndex(Schema, Object) + * @throws org.apache.avro.UnresolvedUnionException if {@code unionSchema} does not have a schema that can encode {@code datum} + */ + public static Schema resolveUnionSchema(Schema unionSchema, Object datum) { + return unionSchema.getTypes().get(resolveUnionIndex(unionSchema, datum)); + } + /* /********************************************************** /* Implementations diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java index 02e45939e..a06b005d2 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/NonBSGenericDatumWriter.java @@ -4,7 +4,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; -import java.util.List; import org.apache.avro.Schema; import org.apache.avro.Schema.Type; @@ -12,8 +11,6 @@ import org.apache.avro.generic.GenericDatumWriter; import org.apache.avro.io.Encoder; -import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; - /** * Need to sub-class to prevent encoder from crapping on writing an optional * Enum value (see [dataformat-avro#12]) @@ -31,52 +28,7 @@ public NonBSGenericDatumWriter(Schema root) { @Override public int resolveUnion(Schema union, Object datum) { - // Alas, we need a work-around first... - if (datum == null) { - return union.getIndexNamed(Type.NULL.getName()); - } - List schemas = union.getTypes(); - if (datum instanceof String) { // String or Enum or Character or char[] - for (int i = 0, len = schemas.size(); i < len; i++) { - Schema s = schemas.get(i); - switch (s.getType()) { - case STRING: - case ENUM: - return i; - case INT: - // Avro distinguishes between String and Character, whereas Jackson doesn't - // Check if the schema is expecting a Character and handle appropriately - if (Character.class.getName().equals(s.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS))) { - return i; - } - break; - case ARRAY: - // Avro distinguishes between String and char[], whereas Jackson doesn't - // Check if the schema is expecting a char[] and handle appropriately - if (s.getElementType().getType() == Type.INT && Character.class - .getName().equals(s.getElementType().getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS))) { - return i; - } - break; - default: - } - } - } else if (datum instanceof BigDecimal) { - int subOptimal = -1; - for (int i = 0, len = schemas.size(); i < len; i++) { - if (schemas.get(i).getType() == Type.STRING) { - return i; - } - if (schemas.get(i).getType() == Type.DOUBLE) { - subOptimal = i; - } - } - if (subOptimal > -1) { - return subOptimal; - } - } - // otherwise just default to base impl, stupid as it is... - return super.resolveUnion(union, datum); + return AvroWriteContext.resolveUnionIndex(union, datum); } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ObjectWriteContext.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ObjectWriteContext.java index a45b27601..45cf8d8c1 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ObjectWriteContext.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/ObjectWriteContext.java @@ -1,14 +1,15 @@ package com.fasterxml.jackson.dataformat.avro.ser; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.dataformat.avro.AvroGenerator; +import java.nio.ByteBuffer; +import java.util.Arrays; + import org.apache.avro.Schema; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericRecord; -import java.nio.ByteBuffer; -import java.util.Arrays; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.avro.AvroGenerator; public final class ObjectWriteContext extends KeyValueContext @@ -57,6 +58,18 @@ public final AvroWriteContext createChildObjectContext() throws JsonMappingExcep return child; } + @Override + public AvroWriteContext createChildObjectContext(Object object) throws JsonMappingException { + _verifyValueWrite(); + Schema.Field field = _findField(); + if (field == null) { // unknown, to ignore + return new NopWriteContext(TYPE_OBJECT, this, _generator); + } + AvroWriteContext child = _createObjectContext(field.schema(), object); + _record.put(_currentName, child.rawValue()); + return child; + } + @Override public final boolean writeFieldName(String name) { diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java new file mode 100644 index 000000000..845d63206 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java @@ -0,0 +1,119 @@ +package com.fasterxml.jackson.dataformat.avro.interop.annotations; + +import java.util.Arrays; +import java.util.List; + +import org.apache.avro.reflect.Nullable; +import org.apache.avro.reflect.Union; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import static org.assertj.core.api.Assertions.assertThat; + +import static com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil.jacksonDeserializer; + +/** + * Tests for @Union + */ +public class UnionTest extends InteropTestBase { + + @Union({ Cat.class, Dog.class }) + public interface Animal { + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Cat implements Animal { + + @Nullable + private String color; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Dog implements Animal { + + private int size; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Bird implements Animal { + + private boolean flying; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Cage { + + private Animal animal; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class PetShop { + + public PetShop(Animal... pets) { + this(Arrays.asList(pets)); + } + + private List pets; + } + + /* + * Jackson deserializer doesn't understand native TypeIDs yet, so it can't handle deserialization of unions. + */ + @Before + public void ignoreJacksonDeserializer() { + Assume.assumeTrue(deserializeFunctor != jacksonDeserializer); + } + + @Test + public void testInterfaceUnionWithCat() { + Cage cage = new Cage(new Cat("test")); + // + Cage result = roundTrip(cage); + // + assertThat(result).isEqualTo(cage); + } + + @Test + public void testInterfaceUnionWithDog() { + Cage cage = new Cage(new Dog(4)); + // + Cage result = roundTrip(cage); + // + assertThat(result).isEqualTo(cage); + } + + @Test(expected = Exception.class) + public void testInterfaceUnionWithBird() { + Cage cage = new Cage(new Bird(true)); + // + roundTrip(cage); + } + + @Test + public void testListWithInterfaceUnion() { + PetShop shop = new PetShop(new Cat("tabby"), new Dog(4), new Dog(5), new Cat("calico")); + // + PetShop result = roundTrip(shop); + // + assertThat(result).isEqualTo(shop); + } + +} From b31da0c8f3e02fbc9e4e6944c7fe1fd169a9c198 Mon Sep 17 00:00:00 2001 From: Bryan Harclerode Date: Mon, 27 Feb 2017 20:21:51 -0600 Subject: [PATCH 2/2] [Avro] Add support for @Union deserialization --- .../avro/AvroAnnotationIntrospector.java | 31 ++- .../jackson/dataformat/avro/AvroMapper.java | 6 +- .../jackson/dataformat/avro/AvroModule.java | 2 + .../jackson/dataformat/avro/AvroParser.java | 11 +- .../dataformat/avro/AvroTypeDeserializer.java | 74 +++++++ .../dataformat/avro/AvroTypeIdResolver.java | 63 ++++++ .../avro/AvroTypeResolverBuilder.java | 50 +++++ .../avro/AvroUntypedDeserializer.java | 131 ++++++++++++ .../dataformat/avro/deser/ArrayReader.java | 42 ++-- .../avro/deser/AvroFieldReader.java | 15 +- .../avro/deser/AvroReadContext.java | 9 +- .../avro/deser/AvroReaderFactory.java | 77 +++++--- .../avro/deser/AvroStructureReader.java | 4 +- .../dataformat/avro/deser/MapReader.java | 49 +++-- .../dataformat/avro/deser/MissingReader.java | 2 +- .../dataformat/avro/deser/RecordReader.java | 42 ++-- .../dataformat/avro/deser/RootReader.java | 7 +- .../dataformat/avro/deser/ScalarDecoder.java | 186 +++++++++++------- .../avro/deser/ScalarDecoderWrapper.java | 7 +- .../dataformat/avro/deser/ScalarDefaults.java | 21 +- .../dataformat/avro/deser/StructDefaults.java | 4 +- .../dataformat/avro/deser/UnionReader.java | 2 +- .../avro/schema/AvroSchemaHelper.java | 62 +++++- .../dataformat/avro/schema/MapVisitor.java | 22 +-- .../dataformat/avro/schema/StringVisitor.java | 2 +- .../dataformat/avro/ser/AvroWriteContext.java | 4 + .../avro/interop/InteropTestBase.java | 4 - .../avro/interop/annotations/UnionTest.java | 40 ++-- .../avro/interop/maps/MapSubtypeTest.java | 4 + .../RecordWithPrimitiveWrapperTest.java | 4 +- 30 files changed, 753 insertions(+), 224 deletions(-) create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeDeserializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeIdResolver.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeResolverBuilder.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroUntypedDeserializer.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java index 0e8ecdf63..d52551ea2 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java @@ -7,16 +7,21 @@ import org.apache.avro.reflect.*; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.core.Version; import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.PropertyName; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.Annotated; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; + /** * Adds support for the following annotations from the Apache Avro implementation: *
    @@ -73,7 +78,7 @@ public List findPropertyAliases(Annotated m) { } protected PropertyName _findName(Annotated a) - { + { AvroName ann = _findAnnotation(a, AvroName.class); return (ann == null) ? null : PropertyName.construct(ann.value()); } @@ -123,4 +128,28 @@ public List findSubtypes(Annotated a) { } return names; } + + @Override + public TypeResolverBuilder findTypeResolver(MapperConfig config, AnnotatedClass ac, JavaType baseType) { + return _findTypeResolver(config, ac, baseType); + } + + @Override + public TypeResolverBuilder findPropertyTypeResolver(MapperConfig config, AnnotatedMember am, JavaType baseType) { + return _findTypeResolver(config, am, baseType); + } + + @Override + public TypeResolverBuilder findPropertyContentTypeResolver(MapperConfig config, AnnotatedMember am, JavaType containerType) { + return _findTypeResolver(config, am, containerType); + } + + protected TypeResolverBuilder _findTypeResolver(MapperConfig config, Annotated ann, JavaType baseType) { + TypeResolverBuilder resolver = new AvroTypeResolverBuilder(); + JsonTypeInfo typeInfo = ann.getAnnotation(JsonTypeInfo.class); + if (typeInfo != null && typeInfo.defaultImpl() != JsonTypeInfo.class) { + resolver = resolver.defaultImpl(typeInfo.defaultImpl()); + } + return resolver; + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroMapper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroMapper.java index 1d301e6af..c9f4e68c7 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroMapper.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroMapper.java @@ -1,16 +1,16 @@ package com.fasterxml.jackson.dataformat.avro; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import org.apache.avro.Schema; import com.fasterxml.jackson.core.Version; - import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; - import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator; /** diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java index b071517ed..e18eca0da 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java @@ -37,6 +37,8 @@ public AvroModule() addSerializer(File.class, new ToStringSerializer(File.class)); // 08-Mar-2016, tatu: to fix [dataformat-avro#35], need to prune 'schema' property: setSerializerModifier(new AvroSerializerModifier()); + // Override untyped deserializer to one that checks for type information in the schema before going to default handling + addDeserializer(Object.class, new AvroUntypedDeserializer()); } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java index 34f6a3a38..8a7e026ab 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java @@ -1,7 +1,6 @@ package com.fasterxml.jackson.dataformat.avro; import java.io.IOException; -import java.io.InputStream; import java.io.Writer; import java.math.BigDecimal; @@ -229,6 +228,16 @@ public void setSchema(FormatSchema schema) protected abstract void _initSchema(AvroSchema schema) throws JsonProcessingException; + @Override + public boolean canReadTypeId() { + return true; + } + + @Override + public Object getTypeId() throws IOException { + return _avroContext != null ? _avroContext.getTypeId() : null; + } + /* /********************************************************** /* Location info diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeDeserializer.java new file mode 100644 index 000000000..cb987d1b1 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeDeserializer.java @@ -0,0 +1,74 @@ +package com.fasterxml.jackson.dataformat.avro; + +import java.io.IOException; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; + +public class AvroTypeDeserializer extends TypeDeserializerBase { + + protected AvroTypeDeserializer(JavaType baseType, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible, + JavaType defaultImpl) { + super(baseType, idRes, typePropertyName, typeIdVisible, defaultImpl); + } + + protected AvroTypeDeserializer(TypeDeserializerBase src, BeanProperty property) { + super(src, property); + } + + @Override + public TypeDeserializer forProperty(BeanProperty prop) { + return new AvroTypeDeserializer(this, prop); + } + + @Override + public JsonTypeInfo.As getTypeInclusion() { + // Don't do any restructuring of the incoming JSON tokens + return JsonTypeInfo.As.EXISTING_PROPERTY; + } + + @Override + public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserializeTypedFromAny(p, ctxt); + } + + @Override + public Object deserializeTypedFromArray(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserializeTypedFromAny(p, ctxt); + } + + @Override + public Object deserializeTypedFromScalar(JsonParser p, DeserializationContext ctxt) throws IOException { + return deserializeTypedFromAny(p, ctxt); + } + + @Override + public Object deserializeTypedFromAny(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.getTypeId() == null && getDefaultImpl() == null) { + JsonDeserializer deser = _findDeserializer(ctxt, AvroSchemaHelper.getTypeId(_baseType)); + if (deser == null) { + ctxt.reportInputMismatch(_baseType, "No (native) type id found when one was expected for polymorphic type handling"); + return null; + } + return deser.deserialize(p, ctxt); + } + return _deserializeWithNativeTypeId(p, ctxt, p.getTypeId()); + } + + @Override + protected JavaType _handleUnknownTypeId(DeserializationContext ctxt, String typeId) + throws IOException { + if (ctxt.hasValueDeserializerFor(_baseType, null)) { + return _baseType; + } + return super._handleUnknownTypeId(ctxt, typeId); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeIdResolver.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeIdResolver.java new file mode 100644 index 000000000..2c89ec89a --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeIdResolver.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.dataformat.avro; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver; +import com.fasterxml.jackson.databind.type.TypeFactory; + +/** + * {@link com.fasterxml.jackson.databind.jsontype.TypeIdResolver} for Avro type IDs embedded in schemas. Avro generally uses class names, + * but we want to also support named subtypes so that developers can easily remap the embedded type IDs to a different runtime class. + */ +public class AvroTypeIdResolver extends ClassNameIdResolver { + + private final Map> _idTypes = new HashMap<>(); + + private final Map, String> _typeIds = new HashMap<>(); + + public AvroTypeIdResolver(JavaType baseType, TypeFactory typeFactory, Collection subTypes) { + this(baseType, typeFactory); + if (subTypes != null) { + for (NamedType namedType : subTypes) { + registerSubtype(namedType.getType(), namedType.getName()); + } + } + } + + public AvroTypeIdResolver(JavaType baseType, TypeFactory typeFactory) { + super(baseType, typeFactory); + } + + @Override + public void registerSubtype(Class type, String name) { + _idTypes.put(name, type); + _typeIds.put(type, name); + } + + @Override + protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOException { + // base types don't have subclasses + if (_baseType.isPrimitive()) { + return _baseType; + } + // check if there's a specific type we should be using for this ID + Class subType = _idTypes.get(id); + if (subType != null) { + id = _idFrom(null, subType, _typeFactory); + } + try { + return super._typeFromId(id, ctxt); + } catch (InvalidTypeIdException | IllegalArgumentException e) { + // AvroTypeDeserializer expects null if we can't map the type ID to a class; It will throw an appropriate error if we can't + // find a usable type. + return null; + } + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeResolverBuilder.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeResolverBuilder.java new file mode 100644 index 000000000..8c9bc7733 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroTypeResolverBuilder.java @@ -0,0 +1,50 @@ +package com.fasterxml.jackson.dataformat.avro; + +import java.util.Collection; + +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.jsontype.NamedType; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder; + + +public class AvroTypeResolverBuilder extends StdTypeResolverBuilder { + + public AvroTypeResolverBuilder() { + super(); + typeIdVisibility(false).typeProperty("@class"); + } + + @Override + public TypeSerializer buildTypeSerializer(SerializationConfig config, JavaType baseType, Collection subtypes) { + // All type information is encoded in the schema, never in the data. + return null; + } + + @Override + public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection subtypes) { + JavaType defaultImpl = null; + if (getDefaultImpl() != null) { + defaultImpl = config.constructType(getDefaultImpl()); + } + + return new AvroTypeDeserializer(baseType, + idResolver(config, baseType, subtypes, true, true), + getTypeProperty(), + isTypeIdVisible(), + defaultImpl + ); + + } + + @Override + protected TypeIdResolver idResolver(MapperConfig config, JavaType baseType, Collection subtypes, boolean forSer, + boolean forDeser) { + return new AvroTypeIdResolver(baseType, config.getTypeFactory(), subtypes); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroUntypedDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroUntypedDeserializer.java new file mode 100644 index 000000000..c7ea27ac2 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroUntypedDeserializer.java @@ -0,0 +1,131 @@ +package com.fasterxml.jackson.dataformat.avro; + +import java.io.IOException; +import java.util.LinkedHashMap; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; + +/** + * Deserializer for handling when we have no target type, just schema type. Normally, the {@link UntypedObjectDeserializer} doesn't look + * for native type information when handling scalar values, but Avro sometimes includes type information in the schema for scalar values; + * This subclass checks for the presence of valid type information and calls out to the type deserializer even for scalar values. The + * same goes for map keys. + */ +public class AvroUntypedDeserializer extends UntypedObjectDeserializer { + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + // Make sure we have a native type ID *AND* that we can resolve it to a type; otherwise, we'll end up in a recursive loop + if (p.canReadTypeId() && p.getTypeId() != null) { + TypeDeserializer deser = ctxt.getConfig().findTypeDeserializer(ctxt.constructType(Object.class)); + if (deser != null) { + TypeIdResolver resolver = deser.getTypeIdResolver(); + if (resolver != null && resolver.typeFromId(ctxt, p.getTypeId().toString()) != null) { + return deser.deserializeTypedFromAny(p, ctxt); + } + } + } + return super.deserialize(p, ctxt); + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { + // Use type deserializer if we have type information, even for scalar values + if (p.canReadTypeId() && p.getTypeId() != null && typeDeserializer != null) { + TypeIdResolver resolver = typeDeserializer.getTypeIdResolver(); + // Make sure that we actually can resolve the type ID, otherwise we'll end up in a recursive loop + if (resolver != null && resolver.typeFromId(ctxt, p.getTypeId().toString()) != null) { + return typeDeserializer.deserializeTypedFromAny(p, ctxt); + } + } + return super.deserializeWithType(p, ctxt, typeDeserializer); + } + + /** + * Method called to map a JSON Object into a Java value. + */ + // Would we just be better off deferring to the Map deserializer? + protected Object mapObject(JsonParser p, DeserializationContext ctxt) throws IOException { + Object key1; + JsonToken t = p.getCurrentToken(); + + if (t == JsonToken.START_OBJECT) { + + key1 = p.nextFieldName(); + } else if (t == JsonToken.FIELD_NAME) { + key1 = p.getCurrentName(); + } else { + if (t != JsonToken.END_OBJECT) { + return ctxt.handleUnexpectedToken(handledType(), p); + } + key1 = null; + } + if (key1 == null) { + // empty map might work; but caller may want to modify... so better just give small modifiable + return new LinkedHashMap<>(2); + } + + KeyDeserializer deserializer = null; + if (p.getTypeId() != null) { + TypeDeserializer typeDeserializer = ctxt.getConfig().findTypeDeserializer(ctxt.constructType(Object.class)); + if (typeDeserializer != null) { + TypeIdResolver idResolver = typeDeserializer.getTypeIdResolver(); + JavaType keyType = idResolver.typeFromId(ctxt, p.getTypeId().toString()); + if (keyType != null) { + deserializer = ctxt.findKeyDeserializer(keyType, null); + } + } + } + if (deserializer != null) { + key1 = deserializer.deserializeKey(key1.toString(), ctxt); + } + // minor optimization; let's handle 1 and 2 entry cases separately + // 24-Mar-2015, tatu: Ideally, could use one of 'nextXxx()' methods, but for + // that we'd need new method(s) in JsonDeserializer. So not quite yet. + p.nextToken(); + Object value1 = deserialize(p, ctxt); + + Object key2 = p.nextFieldName(); + if (key2 == null) { // has to be END_OBJECT, then + // single entry; but we want modifiable + LinkedHashMap result = new LinkedHashMap<>(2); + result.put(key1, value1); + return result; + } + if (deserializer != null) { + key2 = deserializer.deserializeKey(key2.toString(), ctxt); + } + + p.nextToken(); + Object value2 = deserialize(p, ctxt); + + Object key = p.nextFieldName(); + + if (key == null) { + LinkedHashMap result = new LinkedHashMap<>(4); + result.put(key1, value1); + result.put(key2, value2); + return result; + } + // And then the general case; default map size is 16 + LinkedHashMap result = new LinkedHashMap<>(); + result.put(key1, value1); + result.put(key2, value2); + + do { + if (deserializer != null) { + key = deserializer.deserializeKey(key.toString(), ctxt); + } + p.nextToken(); + result.put(key, deserialize(p, ctxt)); + } while ((key = p.nextFieldName()) != null); + return result; + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ArrayReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ArrayReader.java index 79e9cc022..9269f9fa6 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ArrayReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ArrayReader.java @@ -12,24 +12,26 @@ abstract class ArrayReader extends AvroStructureReader protected final static int STATE_DONE = 3; protected final AvroParserImpl _parser; + protected final String _elementTypeId; protected int _state; protected long _count; protected String _currentName; - protected ArrayReader(AvroReadContext parent, AvroParserImpl parser) + protected ArrayReader(AvroReadContext parent, AvroParserImpl parser, String typeId, String elementTypeId) { - super(parent, TYPE_ARRAY); + super(parent, TYPE_ARRAY, typeId); _parser = parser; + _elementTypeId = elementTypeId; } - public static ArrayReader construct(ScalarDecoder reader) { - return new Scalar(reader); + public static ArrayReader construct(ScalarDecoder reader, String typeId, String elementTypeId) { + return new Scalar(reader, typeId, elementTypeId); } - public static ArrayReader construct(AvroStructureReader reader) { - return new NonScalar(reader); + public static ArrayReader construct(AvroStructureReader reader, String typeId, String elementTypeId) { + return new NonScalar(reader, typeId, elementTypeId); } @Override @@ -52,7 +54,12 @@ protected void appendDesc(StringBuilder sb) { sb.append(getCurrentIndex()); sb.append(']'); } - + + @Override + public String getTypeId() { + return _currToken != JsonToken.START_ARRAY && _currToken != JsonToken.END_ARRAY ? _elementTypeId : super.getTypeId(); + } + /* /********************************************************************** /* Reader implementations for Avro arrays @@ -63,19 +70,19 @@ private final static class Scalar extends ArrayReader { private final ScalarDecoder _elementReader; - public Scalar(ScalarDecoder reader) { - this(null, reader, null); + public Scalar(ScalarDecoder reader, String typeId, String elementTypeId) { + this(null, reader, null, typeId, elementTypeId != null ? elementTypeId : reader.getTypeId()); } private Scalar(AvroReadContext parent, ScalarDecoder reader, - AvroParserImpl parser) { - super(parent, parser); + AvroParserImpl parser, String typeId, String elementTypeId) { + super(parent, parser, typeId, elementTypeId != null ? elementTypeId : reader.getTypeId()); _elementReader = reader; } @Override public Scalar newReader(AvroReadContext parent, AvroParserImpl parser) { - return new Scalar(parent, _elementReader, parser); + return new Scalar(parent, _elementReader, parser, _typeId, _elementTypeId); } @Override @@ -130,20 +137,21 @@ private final static class NonScalar extends ArrayReader { private final AvroStructureReader _elementReader; - public NonScalar(AvroStructureReader reader) { - this(null, reader, null); + public NonScalar(AvroStructureReader reader, String typeId, String elementTypeId) { + this(null, reader, null, typeId, elementTypeId); } private NonScalar(AvroReadContext parent, - AvroStructureReader reader, AvroParserImpl parser) { - super(parent, parser); + AvroStructureReader reader, + AvroParserImpl parser, String typeId, String elementTypeId) { + super(parent, parser, typeId, elementTypeId); _elementReader = reader; } @Override public NonScalar newReader(AvroReadContext parent, AvroParserImpl parser) { - return new NonScalar(parent, _elementReader, parser); + return new NonScalar(parent, _elementReader, parser, _typeId, _elementTypeId); } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroFieldReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroFieldReader.java index df9aaa957..bafa8af71 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroFieldReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroFieldReader.java @@ -12,10 +12,12 @@ public abstract class AvroFieldReader { protected final String _name; protected final boolean _isSkipper; + protected final String _typeId; - protected AvroFieldReader(String name, boolean isSkipper) { + protected AvroFieldReader(String name, boolean isSkipper, String typeId) { _name = name; _isSkipper = isSkipper; + _typeId = typeId; } public static AvroFieldReader construct(String name, AvroStructureReader structureReader) { @@ -33,6 +35,10 @@ public static AvroFieldReader constructSkipper(String name, AvroStructureReader public abstract void skipValue(AvroParserImpl parser) throws IOException; + public String getTypeId() { + return _typeId; + } + /** * Implementation used for non-scalar-valued (structured) fields */ @@ -40,7 +46,7 @@ private final static class Structured extends AvroFieldReader { protected final AvroStructureReader _reader; public Structured(String name, boolean skipper, AvroStructureReader r) { - super(name, skipper); + super(name, skipper, null); _reader = r; } @@ -54,5 +60,10 @@ public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws public void skipValue(AvroParserImpl parser) throws IOException { _reader.skipValue(parser); } + + @Override + public String getTypeId() { + return _reader.getTypeId(); + } } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReadContext.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReadContext.java index 372295c8b..bb1007f8f 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReadContext.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReadContext.java @@ -13,16 +13,19 @@ public abstract class AvroReadContext extends JsonStreamContext { protected final AvroReadContext _parent; + protected final String _typeId; + /* /********************************************************************** /* Instance construction /********************************************************************** */ - public AvroReadContext(AvroReadContext parent) + public AvroReadContext(AvroReadContext parent, String typeId) { super(); _parent = parent; + _typeId = typeId; } public abstract JsonToken nextToken() throws IOException; @@ -45,6 +48,10 @@ public AvroReadContext(AvroReadContext parent) protected abstract void appendDesc(StringBuilder sb); + public String getTypeId() { + return _typeId; + } + // !!! TODO: implement from here /** * @since 2.8.7 diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java index 7cad82161..517e7fc2c 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroReaderFactory.java @@ -22,7 +22,6 @@ public abstract class AvroReaderFactory protected final static ScalarDecoder READER_LONG = new LongReader(); protected final static ScalarDecoder READER_NULL = new NullReader(); protected final static ScalarDecoder READER_STRING = new StringReader(); - protected final static ScalarDecoder READER_CHAR = new CharReader(); /** * To resolve cyclic types, need to keep track of resolved named @@ -62,21 +61,24 @@ public ScalarDecoder createScalarValueDecoder(Schema type) case DOUBLE: return READER_DOUBLE; case ENUM: - return new EnumDecoder(type.getFullName(), type.getEnumSymbols()); + return new EnumDecoder(AvroSchemaHelper.getFullName(type), type.getEnumSymbols()); case FIXED: - return new FixedDecoder(type.getFixedSize()); + return new FixedDecoder(type.getFixedSize(), AvroSchemaHelper.getFullName(type)); case FLOAT: return READER_FLOAT; case INT: - if (Character.class.getName().equals(type.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS))) { - return READER_CHAR; + if (AvroSchemaHelper.getTypeId(type) != null) { + return new IntReader(AvroSchemaHelper.getTypeId(type)); } return READER_INT; case LONG: return READER_LONG; case NULL: return READER_NULL; - case STRING: + case STRING: + if (AvroSchemaHelper.getTypeId(type) != null) { + return new StringReader(AvroSchemaHelper.getTypeId(type)); + } return READER_STRING; case UNION: /* Union is a "scalar union" if all the alternative types @@ -118,7 +120,7 @@ public ScalarDecoder createScalarValueDecoder(Schema type) */ public AvroStructureReader createReader(Schema schema) { - AvroStructureReader reader = _knownReaders.get(_typeName(schema)); + AvroStructureReader reader = _knownReaders.get(AvroSchemaHelper.getFullName(schema)); if (reader != null) { return reader; } @@ -141,28 +143,45 @@ protected AvroStructureReader createArrayReader(Schema schema) { Schema elementType = schema.getElementType(); ScalarDecoder scalar = createScalarValueDecoder(elementType); + String typeId = AvroSchemaHelper.getTypeId(schema); + String elementTypeId = schema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_ELEMENT_CLASS); + if (elementTypeId == null) { + elementTypeId = AvroSchemaHelper.getTypeId(elementType); + } + if (scalar != null) { - return ArrayReader.construct(scalar); + // EnumSet has to know element type information up front; take advantage of the fact that the id resolver handles canonical IDs + if (EnumSet.class.getName().equals(typeId)) { + typeId += "<" + elementTypeId + ">"; + } + return ArrayReader.construct(scalar, typeId, elementTypeId); } - return ArrayReader.construct(createReader(elementType)); + return ArrayReader.construct(createReader(elementType), typeId, elementTypeId); } protected AvroStructureReader createMapReader(Schema schema) { Schema elementType = schema.getValueType(); ScalarDecoder dec = createScalarValueDecoder(elementType); + String typeId = AvroSchemaHelper.getTypeId(schema); + String keyTypeId = schema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_KEY_CLASS); + // EnumMap requires value type information up front; take advantage of the fact that the id resolver handles canonical IDs + if (EnumMap.class.getName().equals(typeId)) { + typeId += "<" + keyTypeId + "," + Object.class.getName() + ">"; + } if (dec != null) { - return MapReader.construct(dec); + String valueTypeId = AvroSchemaHelper.getTypeId(elementType); + return MapReader.construct(dec, typeId, keyTypeId, valueTypeId); } - return MapReader.construct(createReader(elementType)); + return MapReader.construct(createReader(elementType), typeId, keyTypeId); } protected AvroStructureReader createRecordReader(Schema schema) { final List fields = schema.getFields(); AvroFieldReader[] fieldReaders = new AvroFieldReader[fields.size()]; - RecordReader reader = new RecordReader.Std(fieldReaders); - _knownReaders.put(_typeName(schema), reader); + RecordReader reader = new RecordReader.Std(fieldReaders, AvroSchemaHelper.getTypeId(schema)); + _knownReaders.put(AvroSchemaHelper.getFullName(schema), reader); int i = 0; for (Schema.Field field : fields) { fieldReaders[i++] = createFieldReader(field); @@ -192,16 +211,6 @@ protected AvroFieldReader createFieldReader(Schema.Field field) { return AvroFieldReader.construct(name, createReader(type)); } - /* - /********************************************************************** - /* Internal methods - /********************************************************************** - */ - - protected final String _typeName(Schema schema) { - return schema.getFullName(); - } - /* /********************************************************************** /* Implementations @@ -234,7 +243,7 @@ public AvroStructureReader createReader(Schema writerSchema, Schema readerSchema { // NOTE: it is assumed writer-schema has been modified with aliases so // that the names are same, so we could use either name: - AvroStructureReader reader = _knownReaders.get(_typeName(readerSchema)); + AvroStructureReader reader = _knownReaders.get(AvroSchemaHelper.getFullName(readerSchema)); if (reader != null) { return reader; } @@ -260,11 +269,13 @@ protected AvroStructureReader createArrayReader(Schema writerSchema, Schema read readerSchema = _verifyMatchingStructure(readerSchema, writerSchema); Schema writerElementType = writerSchema.getElementType(); ScalarDecoder scalar = createScalarValueDecoder(writerElementType); + String typeId = AvroSchemaHelper.getTypeId(readerSchema); + String elementTypeId = readerSchema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_ELEMENT_CLASS); + if (scalar != null) { - return ArrayReader.construct(scalar); + return ArrayReader.construct(scalar, typeId, elementTypeId); } - return ArrayReader.construct(createReader(writerElementType, - readerSchema.getElementType())); + return ArrayReader.construct(createReader(writerElementType, readerSchema.getElementType()), typeId, elementTypeId); } protected AvroStructureReader createMapReader(Schema writerSchema, Schema readerSchema) @@ -272,11 +283,13 @@ protected AvroStructureReader createMapReader(Schema writerSchema, Schema reader readerSchema = _verifyMatchingStructure(readerSchema, writerSchema); Schema writerElementType = writerSchema.getValueType(); ScalarDecoder dec = createScalarValueDecoder(writerElementType); + String typeId = AvroSchemaHelper.getTypeId(readerSchema); + String keyTypeId = readerSchema.getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_KEY_CLASS); if (dec != null) { - return MapReader.construct(dec); + String valueTypeId = readerSchema.getValueType().getProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS); + return MapReader.construct(dec, typeId, keyTypeId, valueTypeId); } - return MapReader.construct(createReader(writerElementType, - readerSchema.getElementType())); + return MapReader.construct(createReader(writerElementType, readerSchema.getElementType()), typeId, keyTypeId); } protected AvroStructureReader createRecordReader(Schema writerSchema, Schema readerSchema) @@ -312,10 +325,10 @@ protected AvroStructureReader createRecordReader(Schema writerSchema, Schema rea // ones from writer schema -- some may skip, but there's entry there AvroFieldReader[] fieldReaders = new AvroFieldReader[writerFields.size() + defaultFields.size()]; - RecordReader reader = new RecordReader.Resolving(fieldReaders); + RecordReader reader = new RecordReader.Resolving(fieldReaders, AvroSchemaHelper.getTypeId(readerSchema)); // as per earlier, names should be the same - _knownReaders.put(_typeName(readerSchema), reader); + _knownReaders.put(AvroSchemaHelper.getFullName(readerSchema), reader); int i = 0; for (Schema.Field writerField : writerFields) { Schema.Field readerField = readerFields.get(writerField.name()); diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroStructureReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroStructureReader.java index 3809670e8..b4d2498f6 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroStructureReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/AvroStructureReader.java @@ -14,8 +14,8 @@ public abstract class AvroStructureReader { protected JsonToken _currToken; - protected AvroStructureReader(AvroReadContext parent, int type) { - super(parent); + protected AvroStructureReader(AvroReadContext parent, int type, String typeId) { + super(parent, typeId); _type = type; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MapReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MapReader.java index fdf64a893..9a3d4d954 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MapReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MapReader.java @@ -13,26 +13,30 @@ public abstract class MapReader extends AvroStructureReader protected final static int STATE_DONE = 4; protected final AvroParserImpl _parser; + protected final String _keyTypeId; + protected final String _valueTypeId; protected String _currentName; protected int _state; - protected MapReader() { - this(null, null); + protected MapReader(String typeId, String keyTypeId, String valueTypeId) { + this(null, null, typeId, keyTypeId, valueTypeId); } - protected MapReader(AvroReadContext parent, AvroParserImpl parser) { - super(parent, TYPE_OBJECT); + protected MapReader(AvroReadContext parent, AvroParserImpl parser, String typeId, String keyTypeId, String valueTypeId) { + super(parent, TYPE_OBJECT, typeId); _parser = parser; + _keyTypeId = keyTypeId; + _valueTypeId = valueTypeId; } - public static MapReader construct(ScalarDecoder dec) { - return new Scalar(dec); + public static MapReader construct(ScalarDecoder dec, String typeId, String keyTypeId, String valueTypeId) { + return new Scalar(dec, typeId, keyTypeId, valueTypeId); } - public static MapReader construct(AvroStructureReader reader) { - return new NonScalar(reader); + public static MapReader construct(AvroStructureReader reader, String typeId, String keyTypeId) { + return new NonScalar(reader, typeId, keyTypeId); } @Override @@ -70,6 +74,17 @@ public void appendDesc(StringBuilder sb) sb.append('}'); } + @Override + public String getTypeId() { + if (_currToken == JsonToken.START_OBJECT || _currToken == JsonToken.END_OBJECT) { + return super.getTypeId(); + } + if (_currToken == JsonToken.FIELD_NAME && _state == STATE_VALUE) { + return _keyTypeId; + } + return _valueTypeId; + } + /* /********************************************************************** /* Implementations @@ -81,19 +96,20 @@ private final static class Scalar extends MapReader private final ScalarDecoder _scalarDecoder; protected long _count; - protected Scalar(ScalarDecoder dec) { + protected Scalar(ScalarDecoder dec, String typeId, String keyTypeId, String valueTypeId) { + super(typeId, keyTypeId, valueTypeId != null ? valueTypeId : dec.getTypeId()); _scalarDecoder = dec; } protected Scalar(AvroReadContext parent, - AvroParserImpl parser, ScalarDecoder sd) { - super(parent, parser); + AvroParserImpl parser, ScalarDecoder sd, String typeId, String keyTypeId, String valueTypeId) { + super(parent, parser, typeId, keyTypeId, valueTypeId != null ? valueTypeId : sd.getTypeId()); _scalarDecoder = sd; } @Override public MapReader newReader(AvroReadContext parent, AvroParserImpl parser) { - return new Scalar(parent, parser, _scalarDecoder); + return new Scalar(parent, parser, _scalarDecoder, _typeId, _keyTypeId, _valueTypeId); } @Override @@ -152,19 +168,20 @@ private final static class NonScalar extends MapReader private final AvroStructureReader _structureReader; protected long _count; - public NonScalar(AvroStructureReader reader) { + public NonScalar(AvroStructureReader reader, String typeId, String keyTypeId) { + super(typeId, keyTypeId, null); _structureReader = reader; } public NonScalar(AvroReadContext parent, - AvroParserImpl parser, AvroStructureReader reader) { - super(parent, parser); + AvroParserImpl parser, AvroStructureReader reader, String typeId, String keyTypeId) { + super(parent, parser, typeId, keyTypeId, null); _structureReader = reader; } @Override public MapReader newReader(AvroReadContext parent, AvroParserImpl parser) { - return new NonScalar(parent, parser, _structureReader); + return new NonScalar(parent, parser, _structureReader, _typeId, _keyTypeId); } @Override public JsonToken nextToken() throws IOException diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MissingReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MissingReader.java index 37b01956d..c64e9b707 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MissingReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/MissingReader.java @@ -9,7 +9,7 @@ public class MissingReader extends AvroReadContext public final static MissingReader instance = new MissingReader(); public MissingReader() { - super(null); + super(null, null); _type = TYPE_ROOT; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RecordReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RecordReader.java index 8743e88fc..a9c78787e 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RecordReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RecordReader.java @@ -20,10 +20,9 @@ abstract class RecordReader extends AvroStructureReader protected int _state; protected final int _count; - protected RecordReader(AvroReadContext parent, - AvroFieldReader[] fieldReaders, AvroParserImpl parser) + protected RecordReader(AvroReadContext parent, AvroFieldReader[] fieldReaders, AvroParserImpl parser, String typeId) { - super(parent, TYPE_OBJECT); + super(parent, TYPE_OBJECT, typeId); _fieldReaders = fieldReaders; _parser = parser; _count = fieldReaders.length; @@ -63,6 +62,18 @@ public void appendDesc(StringBuilder sb) sb.append('}'); } + @Override + public String getTypeId() { + if (_currToken == JsonToken.END_OBJECT || _currToken == JsonToken.START_OBJECT) { + return super.getTypeId(); + } + if (_currToken == JsonToken.FIELD_NAME) { + return _fieldReaders[_index].getTypeId(); + } + // When reading a value type ID, the index pointer has already advanced to the field, so look at the previous + return _fieldReaders[_index - 1].getTypeId(); + } + /* /********************************************************************** /* Implementations @@ -72,19 +83,17 @@ public void appendDesc(StringBuilder sb) public final static class Std extends RecordReader { - public Std(AvroFieldReader[] fieldReaders) { - super(null, fieldReaders, null); + public Std(AvroFieldReader[] fieldReaders, String typeId) { + super(null, fieldReaders, null, typeId); } - public Std(AvroReadContext parent, - AvroFieldReader[] fieldReaders, AvroParserImpl parser) { - super(parent, fieldReaders, parser); + public Std(AvroReadContext parent, AvroFieldReader[] fieldReaders, AvroParserImpl parser, String typeId) { + super(parent, fieldReaders, parser, typeId); } @Override - public RecordReader newReader(AvroReadContext parent, - AvroParserImpl parser) { - return new Std(parent, _fieldReaders, parser); + public RecordReader newReader(AvroReadContext parent, AvroParserImpl parser) { + return new Std(parent, _fieldReaders, parser, _typeId); } @Override @@ -148,17 +157,16 @@ public String nextFieldName() throws IOException public final static class Resolving extends RecordReader { - public Resolving(AvroFieldReader[] fieldReaders) { - super(null, fieldReaders, null); + public Resolving(AvroFieldReader[] fieldReaders, String typeId) { + super(null, fieldReaders, null, typeId); } - public Resolving(AvroReadContext parent, - AvroFieldReader[] fieldReaders, AvroParserImpl parser) { - super(parent, fieldReaders, parser); + public Resolving(AvroReadContext parent, AvroFieldReader[] fieldReaders, AvroParserImpl parser, String typeId) { + super(parent, fieldReaders, parser, typeId); } @Override public RecordReader newReader(AvroReadContext parent, AvroParserImpl parser) { - return new Resolving(parent, _fieldReaders, parser); + return new Resolving(parent, _fieldReaders, parser, _typeId); } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RootReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RootReader.java index 85f3cb1ef..472531fbc 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RootReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/RootReader.java @@ -15,7 +15,7 @@ public final class RootReader extends AvroReadContext public RootReader(AvroParserImpl parser, AvroStructureReader valueReader) { - super(null); + super(null, null); _type = TYPE_ROOT; _parser = parser; _valueReader = valueReader; @@ -47,4 +47,9 @@ public String nextFieldName() throws IOException { AvroStructureReader r = _valueReader.newReader(this, _parser); return r.nextFieldName(); } + + @Override + public String getTypeId() { + return _valueReader.getTypeId(); + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java index 5c22a341b..54e15a33d 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoder.java @@ -4,6 +4,7 @@ import java.util.List; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; /** * Helper classes for reading non-structured values, and can thereby usually @@ -19,6 +20,8 @@ protected abstract void skipValue(AvroParserImpl parser) throws IOException; public abstract AvroFieldReader asFieldReader(String name, boolean skipper); + + public abstract String getTypeId(); /* /********************************************************************** @@ -38,14 +41,19 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipBoolean(); } + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(boolean.class); + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -72,14 +80,19 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipDouble(); } + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(double.class); + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -105,14 +118,19 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipFloat(); } + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(float.class); + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -129,9 +147,24 @@ public void skipValue(AvroParserImpl parser) throws IOException { protected final static class IntReader extends ScalarDecoder { + private final String _typeId; + + public IntReader(String typeId) { + _typeId = typeId; + } + + public IntReader() { + this(AvroSchemaHelper.getTypeId(int.class)); + } + @Override public JsonToken decodeValue(AvroParserImpl parser) throws IOException { - return parser.decodeIntToken(); + JsonToken token = parser.decodeIntToken(); + // Character deserializer expects parser.getText() to return something. Make sure it's populated! + if (Character.class.getName().equals(getTypeId())) { + return parser.setString(Character.toString((char) parser.getIntValue())); + } + return token; } @Override @@ -139,14 +172,19 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipInt(); } + @Override + public String getTypeId() { + return _typeId; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } - + private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -172,17 +210,23 @@ public JsonToken decodeValue(AvroParserImpl parser) throws IOException { protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipLong(); } - + + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(long.class); + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } + @Override public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) throws IOException { return parser.decodeLongToken(); @@ -194,41 +238,6 @@ public void skipValue(AvroParserImpl parser) throws IOException { } } } - - protected final static class CharReader extends ScalarDecoder { - @Override - public JsonToken decodeValue(AvroParserImpl parser) throws IOException { - return parser.setString(Character.toString((char)parser.decodeInt())); - } - - @Override - protected void skipValue(AvroParserImpl parser) throws IOException { - // ints use variable-length zigzagging; alas, no native skipping - parser.skipInt(); - } - - @Override - public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); - } - - private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); - } - - @Override - public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) - throws IOException { - return parser.setString(Character.toString((char) parser.decodeInt())); - } - - @Override - public void skipValue(AvroParserImpl parser) throws IOException { - parser.skipInt(); - } - } - } protected final static class NullReader extends ScalarDecoder { @@ -242,6 +251,11 @@ protected void skipValue(AvroParserImpl parser) throws IOException { ; // value implied } + @Override + public String getTypeId() { + return null; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { return new FR(name, skipper); @@ -249,7 +263,7 @@ public AvroFieldReader asFieldReader(String name, boolean skipper) { private final static class FR extends AvroFieldReader { public FR(String name, boolean skipper) { - super(name, skipper); + super(name, skipper, null); } @Override @@ -264,6 +278,16 @@ public void skipValue(AvroParserImpl parser) throws IOException { } protected final static class StringReader extends ScalarDecoder { + private final String _typeId; + + public StringReader(String typeId) { + _typeId = typeId; + } + + public StringReader() { + this(AvroSchemaHelper.getTypeId(String.class)); + } + @Override public JsonToken decodeValue(AvroParserImpl parser) throws IOException { return parser.decodeString(); @@ -274,14 +298,19 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipString(); } + @Override + public String getTypeId() { + return _typeId; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -307,15 +336,19 @@ public JsonToken decodeValue(AvroParserImpl parser) throws IOException { protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipBytes(); } + @Override + public String getTypeId() { + return AvroSchemaHelper.getTypeId(byte[].class); + } @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper); + return new FR(name, skipper, getTypeId()); } private final static class FR extends AvroFieldReader { - public FR(String name, boolean skipper) { - super(name, skipper); + public FR(String name, boolean skipper, String typeId) { + super(name, skipper, typeId); } @Override @@ -357,6 +390,11 @@ private ScalarDecoder _checkIndex(int index) throws IOException { return _readers[index]; } + @Override + public String getTypeId() { + return null; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { return new FR(name, skipper, _readers); @@ -366,7 +404,7 @@ private final static class FR extends AvroFieldReader { public final ScalarDecoder[] _readers; public FR(String name, boolean skipper, ScalarDecoder[] readers) { - super(name, skipper); + super(name, skipper, null); _readers = readers; } @@ -420,16 +458,21 @@ private final String _checkIndex(int index) throws IOException { return _values[index]; } + @Override + public String getTypeId() { + return _name; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper, this); + return new FR(name, skipper, this, _name); } private final static class FR extends AvroFieldReader { protected final String[] _values; - public FR(String name, boolean skipper, EnumDecoder base) { - super(name, skipper); + public FR(String name, boolean skipper, EnumDecoder base, String typeId) { + super(name, skipper, typeId); _values = base._values; } @@ -458,9 +501,11 @@ protected final static class FixedDecoder extends ScalarDecoder { private final int _size; + private final String _typeId; - public FixedDecoder(int fixedSize) { + public FixedDecoder(int fixedSize, String typeId) { _size = fixedSize; + _typeId = typeId; } @Override @@ -473,16 +518,21 @@ protected void skipValue(AvroParserImpl parser) throws IOException { parser.skipFixed(_size); } + @Override + public String getTypeId() { + return _typeId; + } + @Override public AvroFieldReader asFieldReader(String name, boolean skipper) { - return new FR(name, skipper, _size); + return new FR(name, skipper, _size, _typeId); } private final static class FR extends AvroFieldReader { private final int _size; - public FR(String name, boolean skipper, int size) { - super(name, skipper); + public FR(String name, boolean skipper, int size, String typeId) { + super(name, skipper, typeId); _size = size; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoderWrapper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoderWrapper.java index 6a3d82d11..a95b039fe 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoderWrapper.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDecoderWrapper.java @@ -24,7 +24,7 @@ public ScalarDecoderWrapper(ScalarDecoder wrappedReader) { private ScalarDecoderWrapper(AvroReadContext parent, AvroParserImpl parser, ScalarDecoder valueDecoder) { - super(parent, TYPE_ROOT); + super(parent, TYPE_ROOT, null); _valueDecoder = valueDecoder; _parser = parser; } @@ -41,6 +41,11 @@ public JsonToken nextToken() throws IOException return (_currToken = _valueDecoder.decodeValue(_parser)); } + @Override + public String getTypeId() { + return _valueDecoder.getTypeId(); + } + @Override public void skipValue(AvroParserImpl parser) throws IOException { _valueDecoder.skipValue(parser); diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDefaults.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDefaults.java index 94f1f899e..cc67a31b7 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDefaults.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/ScalarDefaults.java @@ -3,6 +3,7 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper; /** * Container for various {@link ScalarDecoder} implementations that are @@ -13,8 +14,8 @@ public class ScalarDefaults protected abstract static class DefaultsBase extends AvroFieldReader { - protected DefaultsBase(String name) { - super(name, false); // false -> not skip-only + protected DefaultsBase(String name, String typeId) { + super(name, false, typeId); // false -> not skip-only } @Override @@ -32,7 +33,7 @@ protected final static class BooleanDefaults extends DefaultsBase protected final JsonToken _defaults; public BooleanDefaults(String name, boolean v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(boolean.class)); _defaults = v ? JsonToken.VALUE_TRUE : JsonToken.VALUE_FALSE; } @@ -47,7 +48,7 @@ protected final static class StringDefaults extends DefaultsBase protected final String _defaults; public StringDefaults(String name, String v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(String.class)); _defaults = v; } @@ -62,7 +63,7 @@ protected final static class BytesDefaults extends DefaultsBase protected final byte[] _defaults; public BytesDefaults(String name, byte[] v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(byte[].class)); _defaults = v; } @@ -77,7 +78,7 @@ protected final static class DoubleDefaults extends DefaultsBase protected final double _defaults; public DoubleDefaults(String name, double v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(double.class)); _defaults = v; } @@ -92,7 +93,7 @@ protected final static class FloatDefaults extends DefaultsBase protected final float _defaults; public FloatDefaults(String name, float v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(float.class)); _defaults = v; } @@ -107,7 +108,7 @@ protected final static class IntDefaults extends DefaultsBase protected final int _defaults; public IntDefaults(String name, int v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(int.class)); _defaults = v; } @@ -122,7 +123,7 @@ protected final static class LongDefaults extends DefaultsBase protected final long _defaults; public LongDefaults(String name, long v) { - super(name); + super(name, AvroSchemaHelper.getTypeId(long.class)); _defaults = v; } @@ -135,7 +136,7 @@ public JsonToken readValue(AvroReadContext parent, AvroParserImpl parser) { protected final static class NullDefaults extends DefaultsBase { public NullDefaults(String name) { - super(name); + super(name, null); } @Override diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/StructDefaults.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/StructDefaults.java index 8de7e7dcb..23c9d6e84 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/StructDefaults.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/StructDefaults.java @@ -36,7 +36,7 @@ protected static class ObjectDefaults extends MapReader public ObjectDefaults(AvroReadContext parent, AvroParserImpl parser, AvroFieldReader[] fieldReaders) { - super(parent, parser); + super(parent, parser, null, null, null); _fieldReaders = fieldReaders; } @@ -87,7 +87,7 @@ protected static class ArrayDefaults extends ArrayReader public ArrayDefaults(AvroReadContext parent, AvroParserImpl parser, AvroFieldReader[] valueReaders) { - super(parent, parser); + super(parent, parser, null, null); _valueReaders = valueReaders; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/UnionReader.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/UnionReader.java index d94ce5d15..4cac67dd7 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/UnionReader.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/deser/UnionReader.java @@ -20,7 +20,7 @@ public UnionReader(AvroStructureReader[] memberReaders) { private UnionReader(AvroReadContext parent, AvroStructureReader[] memberReaders, AvroParserImpl parser) { - super(parent, TYPE_ROOT); + super(parent, TYPE_ROOT, null); _memberReaders = memberReaders; _parser = parser; } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java index 005ae678c..dc373f80e 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaHelper.java @@ -40,6 +40,14 @@ public abstract class AvroSchemaHelper */ public static final String AVRO_SCHEMA_PROP_KEY_CLASS = SpecificData.KEY_CLASS_PROP; + /** + * Constant used by native Avro Schemas for indicating more specific + * physical class of a array element; referenced indirectly to reduce direct + * dependencies to the standard avro library. + * + * @since 2.8.8 + */ + public static final String AVRO_SCHEMA_PROP_ELEMENT_CLASS = SpecificData.ELEMENT_PROP; /** * Default stringable classes * @@ -52,7 +60,7 @@ public abstract class AvroSchemaHelper )); /** - * Checks if a given type is "Stringable", that is one of the default {@link #STRINGABLE_CLASSES}, + * Checks if a given type is "Stringable", that is one of the default {@link #STRINGABLE_CLASSES}, is an {@code Enum}, * or is annotated with * {@link Stringable @Stringable} and has a constructor that takes a single string argument capable of deserializing the output of its * {@code toString()} method. @@ -63,10 +71,10 @@ public abstract class AvroSchemaHelper * @return {@code true} if it can be stored in a string schema, otherwise {@code false} */ public static boolean isStringable(AnnotatedClass type) { - if (STRINGABLE_CLASSES.contains(type.getRawType())) { + if (STRINGABLE_CLASSES.contains(type.getRawType()) || Enum.class.isAssignableFrom(type.getRawType())) { return true; } - if (!type.hasAnnotation(Stringable.class)) { + if (type.getAnnotated().getAnnotation(Stringable.class) == null) { return false; } for (AnnotatedConstructor constructor : type.getConstructors()) { @@ -188,6 +196,17 @@ public static Schema anyNumberSchema() )); } + public static Schema stringableKeyMapSchema(JavaType mapType, JavaType keyType, Schema valueSchema) { + Schema schema = Schema.createMap(valueSchema); + if (mapType != null && !mapType.hasRawClass(Map.class)) { + schema.addProp(AVRO_SCHEMA_PROP_CLASS, getTypeId(mapType)); + } + if (keyType != null && !keyType.hasRawClass(String.class)) { + schema.addProp(AVRO_SCHEMA_PROP_KEY_CLASS, getTypeId(keyType)); + } + return schema; + } + protected static T throwUnsupported() { throw new UnsupportedOperationException("Format variation not supported"); } @@ -238,4 +257,41 @@ public static String getTypeId(Class type) { } return type.getName(); } + + /** + * Returns the type ID for this schema, or {@code null} if none is present. + */ + public static String getTypeId(Schema schema) { + switch (schema.getType()) { + case RECORD: + case ENUM: + case FIXED: + return getFullName(schema); + default: + return schema.getProp(AVRO_SCHEMA_PROP_CLASS); + } + + } + + /** + * Returns the full name of a schema; This is similar to {@link Schema#getFullName()}, except that it properly handles namespaces for + * nested classes. (package.name.ClassName$NestedClassName instead of package.name.ClassName$.NestedClassName) + */ + public static String getFullName(Schema schema) { + switch (schema.getType()) { + case RECORD: + case ENUM: + case FIXED: + String namespace = schema.getNamespace(); + if (namespace == null) { + return schema.getName(); + } + if (namespace.endsWith("$")) { + return String.format("%s%s", namespace, schema.getName()); + } + return String.format("%s.%s", namespace, schema.getName()); + default: + return schema.getType().getName(); + } + } } diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java index 2cc3a5ecb..712192dc7 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/MapVisitor.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor; @@ -32,23 +33,12 @@ public Schema builtAvroSchema() { if (_valueSchema == null) { throw new IllegalStateException("Missing value type for "+_type); } - - Schema schema = Schema.createMap(_valueSchema); - - // add the key type if there is one - if (_keyType != null && AvroSchemaHelper.isStringable(getProvider() - .getConfig() - .introspectClassAnnotations(_keyType) - .getClassInfo())) { - schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_KEY_CLASS, AvroSchemaHelper.getTypeId(_keyType)); - } else if (_keyType != null && !_keyType.isEnumType()) { - // Avro handles non-stringable keys by converting the map to an array of key/value records - // TODO add support for these in the schema, and custom serializers / deserializers to handle map restructuring - throw new UnsupportedOperationException( - "Key " + _keyType + " is not stringable and non-stringable map keys are not supported yet."); + AnnotatedClass ac = _provider.getConfig().introspectClassAnnotations(_keyType).getClassInfo(); + if (AvroSchemaHelper.isStringable(ac)) { + return AvroSchemaHelper.stringableKeyMapSchema(_type, _keyType, _valueSchema); + } else { + throw new UnsupportedOperationException("Maps with non-stringable keys are not supported yet"); } - - return schema; } /* diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java index cfb88439d..e0edde353 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java @@ -52,7 +52,7 @@ public Schema builtAvroSchema() { } Schema schema = Schema.create(Schema.Type.STRING); // Stringable classes need to include the type - if (AvroSchemaHelper.isStringable(bean.getClassInfo())) { + if (AvroSchemaHelper.isStringable(bean.getClassInfo()) && !_type.hasRawClass(String.class)) { schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS, AvroSchemaHelper.getTypeId(_type)); } return schema; diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java index f15561000..627cdd34d 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/ser/AvroWriteContext.java @@ -218,6 +218,10 @@ public static int resolveUnionIndex(Schema unionSchema, Object datum) { int subOptimal = -1; for(int i = 0, size = unionSchema.getTypes().size(); i < size; i++) { Schema schema = unionSchema.getTypes().get(i); + // Exact schema match? + if (AvroSchemaHelper.getTypeId(datum.getClass()).equals(AvroSchemaHelper.getTypeId(schema))) { + return i; + } if (datum instanceof BigDecimal) { // BigDecimals can be shoved into a double, but optimally would be a String or byte[] with logical type information if (schema.getType() == Type.DOUBLE) { diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/InteropTestBase.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/InteropTestBase.java index 5273f9858..5277e065b 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/InteropTestBase.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/InteropTestBase.java @@ -167,10 +167,6 @@ protected T roundTrip(T object) throws IOException { @SuppressWarnings("unchecked") protected T roundTrip(Type schemaType, T object) throws IOException { Schema schema = schemaFunctor.apply(schemaType); - // Temporary hack until jackson supports native type Ids and we don't need to give it a target type - if (deserializeFunctor == ApacheAvroInteropUtil.jacksonDeserializer) { - return ApacheAvroInteropUtil.jacksonDeserialize(schema, schemaType, serializeFunctor.apply(schema, object)); - } return (T) deserializeFunctor.apply(schema, serializeFunctor.apply(schema, object)); } } diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java index 845d63206..5fc98acc7 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/annotations/UnionTest.java @@ -1,23 +1,22 @@ package com.fasterxml.jackson.dataformat.avro.interop.annotations; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.apache.avro.UnresolvedUnionException; import org.apache.avro.reflect.Nullable; import org.apache.avro.reflect.Union; -import org.junit.Assume; -import org.junit.Before; import org.junit.Test; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - import static org.assertj.core.api.Assertions.assertThat; - -import static com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil.jacksonDeserializer; +import static org.junit.Assert.fail; /** * Tests for @Union @@ -74,16 +73,8 @@ public PetShop(Animal... pets) { private List pets; } - /* - * Jackson deserializer doesn't understand native TypeIDs yet, so it can't handle deserialization of unions. - */ - @Before - public void ignoreJacksonDeserializer() { - Assume.assumeTrue(deserializeFunctor != jacksonDeserializer); - } - @Test - public void testInterfaceUnionWithCat() { + public void testInterfaceUnionWithCat() throws IOException { Cage cage = new Cage(new Cat("test")); // Cage result = roundTrip(cage); @@ -92,7 +83,7 @@ public void testInterfaceUnionWithCat() { } @Test - public void testInterfaceUnionWithDog() { + public void testInterfaceUnionWithDog() throws IOException { Cage cage = new Cage(new Dog(4)); // Cage result = roundTrip(cage); @@ -100,15 +91,20 @@ public void testInterfaceUnionWithDog() { assertThat(result).isEqualTo(cage); } - @Test(expected = Exception.class) - public void testInterfaceUnionWithBird() { + @Test + public void testInterfaceUnionWithBird() throws IOException { Cage cage = new Cage(new Bird(true)); // - roundTrip(cage); + try { + roundTrip(cage); + fail("Should throw exception about Bird not being in union"); + } catch (UnresolvedUnionException | JsonMappingException e) { + // success + } } @Test - public void testListWithInterfaceUnion() { + public void testListWithInterfaceUnion() throws IOException { PetShop shop = new PetShop(new Cat("tabby"), new Dog(4), new Dog(5), new Cat("calico")); // PetShop result = roundTrip(shop); diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/maps/MapSubtypeTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/maps/MapSubtypeTest.java index 92c8610c9..304275930 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/maps/MapSubtypeTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/maps/MapSubtypeTest.java @@ -26,7 +26,11 @@ public class MapSubtypeTest extends InteropTestBase { public void ignoreApacheMapSubtypeBug() { // The Apache Avro implementation has a bug that causes all of these tests to fail. Conditionally ignore these tests when running // with Apache deserializer implementation + + // Apache ignores any type information for maps Assume.assumeTrue(deserializeFunctor != apacheDeserializer); + // Apache doesn't encode type information for maps + Assume.assumeTrue(schemaFunctor != getApacheSchema); } @Test diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/records/RecordWithPrimitiveWrapperTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/records/RecordWithPrimitiveWrapperTest.java index 4895f11c6..2bb5ce259 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/records/RecordWithPrimitiveWrapperTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/interop/records/RecordWithPrimitiveWrapperTest.java @@ -1,12 +1,12 @@ package com.fasterxml.jackson.dataformat.avro.interop.records; +import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase; + import lombok.Data; import org.junit.Before; import org.junit.Test; -import com.fasterxml.jackson.dataformat.avro.interop.InteropTestBase; - import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException;