Skip to content

Commit dccd551

Browse files
authored
Merge pull request #60 from baharclerode/bah.UnionSerialization
[Avro] Add support for `@Union` and polymorphic types
2 parents fbe7564 + b31da0c commit dccd551

35 files changed

+1033
-284
lines changed

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroAnnotationIntrospector.java

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
package com.fasterxml.jackson.dataformat.avro;
22

3+
import java.util.ArrayList;
34
import java.util.Collections;
45
import java.util.List;
56

67
import org.apache.avro.reflect.*;
78

89
import com.fasterxml.jackson.annotation.JsonCreator;
10+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
911
import com.fasterxml.jackson.core.Version;
1012
import com.fasterxml.jackson.databind.AnnotationIntrospector;
13+
import com.fasterxml.jackson.databind.JavaType;
1114
import com.fasterxml.jackson.databind.PropertyName;
1215
import com.fasterxml.jackson.databind.cfg.MapperConfig;
1316
import com.fasterxml.jackson.databind.introspect.Annotated;
1417
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
1518
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor;
1619
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
20+
import com.fasterxml.jackson.databind.jsontype.NamedType;
21+
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
1722
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
23+
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
24+
1825
/**
1926
* Adds support for the following annotations from the Apache Avro implementation:
2027
* <ul>
@@ -26,6 +33,7 @@
2633
* <li>{@link Nullable @Nullable} - Alias for <code>JsonProperty(required = false)</code></li>
2734
* <li>{@link Stringable @Stringable} - Alias for <code>JsonCreator</code> on the constructor and <code>JsonValue</code> on
2835
* the {@link #toString()} method. </li>
36+
* <li>{@link Union @Union} - Alias for <code>JsonSubTypes</code></li>
2937
* </ul>
3038
*
3139
* @since 2.9
@@ -70,7 +78,7 @@ public List<PropertyName> findPropertyAliases(Annotated m) {
7078
}
7179

7280
protected PropertyName _findName(Annotated a)
73-
{
81+
{
7482
AvroName ann = _findAnnotation(a, AvroName.class);
7583
return (ann == null) ? null : PropertyName.construct(ann.value());
7684
}
@@ -107,4 +115,41 @@ public Object findSerializer(Annotated a) {
107115
}
108116
return null;
109117
}
118+
119+
@Override
120+
public List<NamedType> findSubtypes(Annotated a) {
121+
Union union = _findAnnotation(a, Union.class);
122+
if (union == null) {
123+
return null;
124+
}
125+
ArrayList<NamedType> names = new ArrayList<>(union.value().length);
126+
for (Class<?> subtype : union.value()) {
127+
names.add(new NamedType(subtype, AvroSchemaHelper.getTypeId(subtype)));
128+
}
129+
return names;
130+
}
131+
132+
@Override
133+
public TypeResolverBuilder<?> findTypeResolver(MapperConfig<?> config, AnnotatedClass ac, JavaType baseType) {
134+
return _findTypeResolver(config, ac, baseType);
135+
}
136+
137+
@Override
138+
public TypeResolverBuilder<?> findPropertyTypeResolver(MapperConfig<?> config, AnnotatedMember am, JavaType baseType) {
139+
return _findTypeResolver(config, am, baseType);
140+
}
141+
142+
@Override
143+
public TypeResolverBuilder<?> findPropertyContentTypeResolver(MapperConfig<?> config, AnnotatedMember am, JavaType containerType) {
144+
return _findTypeResolver(config, am, containerType);
145+
}
146+
147+
protected TypeResolverBuilder<?> _findTypeResolver(MapperConfig<?> config, Annotated ann, JavaType baseType) {
148+
TypeResolverBuilder<?> resolver = new AvroTypeResolverBuilder();
149+
JsonTypeInfo typeInfo = ann.getAnnotation(JsonTypeInfo.class);
150+
if (typeInfo != null && typeInfo.defaultImpl() != JsonTypeInfo.class) {
151+
resolver = resolver.defaultImpl(typeInfo.defaultImpl());
152+
}
153+
return resolver;
154+
}
110155
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroGenerator.java

+26-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
package com.fasterxml.jackson.dataformat.avro;
22

3-
import com.fasterxml.jackson.core.*;
4-
import com.fasterxml.jackson.core.base.GeneratorBase;
5-
import com.fasterxml.jackson.core.io.IOContext;
6-
import com.fasterxml.jackson.dataformat.avro.ser.AvroWriteContext;
7-
8-
import org.apache.avro.io.BinaryEncoder;
9-
103
import java.io.IOException;
114
import java.io.OutputStream;
125
import java.math.BigDecimal;
136
import java.math.BigInteger;
147
import java.nio.ByteBuffer;
158

9+
import org.apache.avro.io.BinaryEncoder;
10+
11+
import com.fasterxml.jackson.core.Base64Variant;
12+
import com.fasterxml.jackson.core.FormatFeature;
13+
import com.fasterxml.jackson.core.FormatSchema;
14+
import com.fasterxml.jackson.core.JsonGenerationException;
15+
import com.fasterxml.jackson.core.JsonGenerator;
16+
import com.fasterxml.jackson.core.ObjectCodec;
17+
import com.fasterxml.jackson.core.PrettyPrinter;
18+
import com.fasterxml.jackson.core.SerializableString;
19+
import com.fasterxml.jackson.core.Version;
20+
import com.fasterxml.jackson.core.base.GeneratorBase;
21+
import com.fasterxml.jackson.core.io.IOContext;
22+
import com.fasterxml.jackson.dataformat.avro.ser.AvroWriteContext;
23+
1624
public class AvroGenerator extends GeneratorBase
1725
{
1826
/**
@@ -381,6 +389,17 @@ public final void writeStartObject() throws IOException {
381389
_complete = false;
382390
}
383391

392+
@Override
393+
public void writeStartObject(Object forValue) throws IOException {
394+
_avroContext = _avroContext.createChildObjectContext(forValue);
395+
_complete = false;
396+
if(this._writeContext != null && forValue != null) {
397+
this._writeContext.setCurrentValue(forValue);
398+
}
399+
400+
this.setCurrentValue(forValue);
401+
}
402+
384403
@Override
385404
public final void writeEndObject() throws IOException
386405
{

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroMapper.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.fasterxml.jackson.dataformat.avro;
22

3-
import java.io.*;
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.InputStream;
46

57
import org.apache.avro.Schema;
68

79
import com.fasterxml.jackson.core.Version;
8-
910
import com.fasterxml.jackson.databind.JavaType;
1011
import com.fasterxml.jackson.databind.JsonMappingException;
1112
import com.fasterxml.jackson.databind.Module;
1213
import com.fasterxml.jackson.databind.ObjectMapper;
13-
1414
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator;
1515

1616
/**

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public AvroModule()
3737
addSerializer(File.class, new ToStringSerializer(File.class));
3838
// 08-Mar-2016, tatu: to fix [dataformat-avro#35], need to prune 'schema' property:
3939
setSerializerModifier(new AvroSerializerModifier());
40+
// Override untyped deserializer to one that checks for type information in the schema before going to default handling
41+
addDeserializer(Object.class, new AvroUntypedDeserializer());
4042
}
4143

4244
@Override

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/AvroParser.java

+10
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ public void setSchema(FormatSchema schema)
228228

229229
protected abstract void _initSchema(AvroSchema schema) throws JsonProcessingException;
230230

231+
@Override
232+
public boolean canReadTypeId() {
233+
return true;
234+
}
235+
236+
@Override
237+
public Object getTypeId() throws IOException {
238+
return _avroContext != null ? _avroContext.getTypeId() : null;
239+
}
240+
231241
/*
232242
/**********************************************************
233243
/* Location info
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.fasterxml.jackson.dataformat.avro;
2+
3+
import java.io.IOException;
4+
5+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
6+
import com.fasterxml.jackson.core.JsonParser;
7+
import com.fasterxml.jackson.databind.BeanProperty;
8+
import com.fasterxml.jackson.databind.DeserializationContext;
9+
import com.fasterxml.jackson.databind.JavaType;
10+
import com.fasterxml.jackson.databind.JsonDeserializer;
11+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
12+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
13+
import com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase;
14+
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaHelper;
15+
16+
public class AvroTypeDeserializer extends TypeDeserializerBase {
17+
18+
protected AvroTypeDeserializer(JavaType baseType, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible,
19+
JavaType defaultImpl) {
20+
super(baseType, idRes, typePropertyName, typeIdVisible, defaultImpl);
21+
}
22+
23+
protected AvroTypeDeserializer(TypeDeserializerBase src, BeanProperty property) {
24+
super(src, property);
25+
}
26+
27+
@Override
28+
public TypeDeserializer forProperty(BeanProperty prop) {
29+
return new AvroTypeDeserializer(this, prop);
30+
}
31+
32+
@Override
33+
public JsonTypeInfo.As getTypeInclusion() {
34+
// Don't do any restructuring of the incoming JSON tokens
35+
return JsonTypeInfo.As.EXISTING_PROPERTY;
36+
}
37+
38+
@Override
39+
public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
40+
return deserializeTypedFromAny(p, ctxt);
41+
}
42+
43+
@Override
44+
public Object deserializeTypedFromArray(JsonParser p, DeserializationContext ctxt) throws IOException {
45+
return deserializeTypedFromAny(p, ctxt);
46+
}
47+
48+
@Override
49+
public Object deserializeTypedFromScalar(JsonParser p, DeserializationContext ctxt) throws IOException {
50+
return deserializeTypedFromAny(p, ctxt);
51+
}
52+
53+
@Override
54+
public Object deserializeTypedFromAny(JsonParser p, DeserializationContext ctxt) throws IOException {
55+
if (p.getTypeId() == null && getDefaultImpl() == null) {
56+
JsonDeserializer<Object> deser = _findDeserializer(ctxt, AvroSchemaHelper.getTypeId(_baseType));
57+
if (deser == null) {
58+
ctxt.reportInputMismatch(_baseType, "No (native) type id found when one was expected for polymorphic type handling");
59+
return null;
60+
}
61+
return deser.deserialize(p, ctxt);
62+
}
63+
return _deserializeWithNativeTypeId(p, ctxt, p.getTypeId());
64+
}
65+
66+
@Override
67+
protected JavaType _handleUnknownTypeId(DeserializationContext ctxt, String typeId)
68+
throws IOException {
69+
if (ctxt.hasValueDeserializerFor(_baseType, null)) {
70+
return _baseType;
71+
}
72+
return super._handleUnknownTypeId(ctxt, typeId);
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.fasterxml.jackson.dataformat.avro;
2+
3+
import java.io.IOException;
4+
import java.util.Collection;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
import com.fasterxml.jackson.databind.DatabindContext;
9+
import com.fasterxml.jackson.databind.JavaType;
10+
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
11+
import com.fasterxml.jackson.databind.jsontype.NamedType;
12+
import com.fasterxml.jackson.databind.jsontype.impl.ClassNameIdResolver;
13+
import com.fasterxml.jackson.databind.type.TypeFactory;
14+
15+
/**
16+
* {@link com.fasterxml.jackson.databind.jsontype.TypeIdResolver} for Avro type IDs embedded in schemas. Avro generally uses class names,
17+
* but we want to also support named subtypes so that developers can easily remap the embedded type IDs to a different runtime class.
18+
*/
19+
public class AvroTypeIdResolver extends ClassNameIdResolver {
20+
21+
private final Map<String, Class<?>> _idTypes = new HashMap<>();
22+
23+
private final Map<Class<?>, String> _typeIds = new HashMap<>();
24+
25+
public AvroTypeIdResolver(JavaType baseType, TypeFactory typeFactory, Collection<NamedType> subTypes) {
26+
this(baseType, typeFactory);
27+
if (subTypes != null) {
28+
for (NamedType namedType : subTypes) {
29+
registerSubtype(namedType.getType(), namedType.getName());
30+
}
31+
}
32+
}
33+
34+
public AvroTypeIdResolver(JavaType baseType, TypeFactory typeFactory) {
35+
super(baseType, typeFactory);
36+
}
37+
38+
@Override
39+
public void registerSubtype(Class<?> type, String name) {
40+
_idTypes.put(name, type);
41+
_typeIds.put(type, name);
42+
}
43+
44+
@Override
45+
protected JavaType _typeFromId(String id, DatabindContext ctxt) throws IOException {
46+
// base types don't have subclasses
47+
if (_baseType.isPrimitive()) {
48+
return _baseType;
49+
}
50+
// check if there's a specific type we should be using for this ID
51+
Class<?> subType = _idTypes.get(id);
52+
if (subType != null) {
53+
id = _idFrom(null, subType, _typeFactory);
54+
}
55+
try {
56+
return super._typeFromId(id, ctxt);
57+
} catch (InvalidTypeIdException | IllegalArgumentException e) {
58+
// AvroTypeDeserializer expects null if we can't map the type ID to a class; It will throw an appropriate error if we can't
59+
// find a usable type.
60+
return null;
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.fasterxml.jackson.dataformat.avro;
2+
3+
import java.util.Collection;
4+
5+
import com.fasterxml.jackson.databind.DeserializationConfig;
6+
import com.fasterxml.jackson.databind.JavaType;
7+
import com.fasterxml.jackson.databind.SerializationConfig;
8+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
9+
import com.fasterxml.jackson.databind.jsontype.NamedType;
10+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
11+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
12+
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
13+
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
14+
15+
16+
public class AvroTypeResolverBuilder extends StdTypeResolverBuilder {
17+
18+
public AvroTypeResolverBuilder() {
19+
super();
20+
typeIdVisibility(false).typeProperty("@class");
21+
}
22+
23+
@Override
24+
public TypeSerializer buildTypeSerializer(SerializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
25+
// All type information is encoded in the schema, never in the data.
26+
return null;
27+
}
28+
29+
@Override
30+
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
31+
JavaType defaultImpl = null;
32+
if (getDefaultImpl() != null) {
33+
defaultImpl = config.constructType(getDefaultImpl());
34+
}
35+
36+
return new AvroTypeDeserializer(baseType,
37+
idResolver(config, baseType, subtypes, true, true),
38+
getTypeProperty(),
39+
isTypeIdVisible(),
40+
defaultImpl
41+
);
42+
43+
}
44+
45+
@Override
46+
protected TypeIdResolver idResolver(MapperConfig<?> config, JavaType baseType, Collection<NamedType> subtypes, boolean forSer,
47+
boolean forDeser) {
48+
return new AvroTypeIdResolver(baseType, config.getTypeFactory(), subtypes);
49+
}
50+
}

0 commit comments

Comments
 (0)