Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Avro] Add support for @AvroDefault and @Nullable annotations #49

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions avro/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ abstractions.
</properties>

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<!-- Hmmh. Need databind for Avro Schema generation... -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
Expand All @@ -32,13 +36,7 @@ abstractions.
<version>1.7.7</version>
</dependency>

<!-- and for testing we need annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>test</scope>
</dependency>
<!-- plus logback -->
<!-- and for testing we need logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.dataformat.avro;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.PropertyName;
Expand All @@ -9,6 +10,7 @@
import org.apache.avro.reflect.AvroDefault;
import org.apache.avro.reflect.AvroIgnore;
import org.apache.avro.reflect.AvroName;
import org.apache.avro.reflect.Nullable;

/**
* Adds support for the following annotations from the Apache Avro implementation:
Expand All @@ -18,6 +20,7 @@
* <li>{@link AvroDefault @AvroDefault("default value")} - Alias for <code>JsonProperty.defaultValue</code>, to
* define default value for generated Schemas
* </li>
* <li>{@link Nullable @Nullable} - Alias for <code>JsonProperty(required = false)</code></li>
* </ul>
*
* @since 2.9
Expand Down Expand Up @@ -57,4 +60,18 @@ protected PropertyName _findName(Annotated a)
AvroName ann = _findAnnotation(a, AvroName.class);
return (ann == null) ? null : PropertyName.construct(ann.value());
}

@Override
public Boolean hasRequiredMarker(AnnotatedMember m) {
if (_hasAnnotation(m, Nullable.class)) {
return false;
}
// Appears to be a bug in POJOPropertyBuilder.getMetadata()
// Can't specify a default unless property is known to be required or not, or we get an NPE
// If we have a default but no annotations indicating required or not, assume required.
if (_hasAnnotation(m, AvroDefault.class) && !_hasAnnotation(m, JsonProperty.class)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think check for @JsonProperty should be needed here.
Is that only used to avoid NPE or for some other need?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only to work around the NPE. I just saw your message about fixing it, so I'll test everything with that and see if I can remove this workaround!

return true;
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.fasterxml.jackson.dataformat.avro.schema;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitable;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.dataformat.avro.AvroFixedSize;
import org.apache.avro.Schema;

import java.util.ArrayList;
import java.util.List;
import org.apache.avro.Schema;
import org.apache.avro.Schema.Type;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;

public class RecordVisitor
extends JsonObjectFormatVisitor.Base
Expand Down Expand Up @@ -50,7 +56,9 @@ public Schema builtAvroSchema() {
public void property(BeanProperty writer) throws JsonMappingException
{
Schema schema = schemaForWriter(writer);
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
JsonNode defaultValue = parseJson(getProvider().getAnnotationIntrospector().findPropertyDefaultValue(writer.getMember()));
schema = reorderUnionToMatchDefaultType(schema, defaultValue);
_fields.add(new Schema.Field(writer.getName(), schema, null, defaultValue));
}

@Override
Expand All @@ -73,7 +81,9 @@ public void optionalProperty(BeanProperty writer) throws JsonMappingException {
if (!writer.getType().isPrimitive()) {
schema = AvroSchemaHelper.unionWithNull(schema);
}
_fields.add(new Schema.Field(writer.getName(), schema, null, null));
JsonNode defaultValue = parseJson(getProvider().getAnnotationIntrospector().findPropertyDefaultValue(writer.getMember()));
schema = reorderUnionToMatchDefaultType(schema, defaultValue);
_fields.add(new Schema.Field(writer.getName(), schema, null, defaultValue));
}

@Override
Expand Down Expand Up @@ -120,4 +130,99 @@ protected Schema schemaForWriter(BeanProperty prop) throws JsonMappingException
ser.acceptJsonFormatVisitor(visitor, prop.getType());
return visitor.getAvroSchema();
}

/**
* Parses a JSON-encoded string for use as the default value of a field
*
* @param defaultValue
* Default value as a JSON-encoded string
*
* @return Jackson V1 {@link JsonNode} for use as the default value in a {@link Schema.Field}
*
* @throws JsonMappingException
* if {@code defaultValue} is not valid JSON
*/
protected JsonNode parseJson(String defaultValue) throws JsonMappingException {
if (defaultValue == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readTree(defaultValue);
} catch (IOException e) {
throw JsonMappingException.from(getProvider(), "Unable to parse default value as JSON: " + defaultValue, e);
}
}

/**
* A union schema with a default value must always have the schema branch corresponding to the default value first, or Avro will print a
* warning complaining that the default value is not compatible. If {@code schema} is a {@link Schema.Type#UNION UNION} schema and
* {@code defaultValue} is non-{@code null}, this finds the appropriate branch in the union and reorders the union so that it is first.
*
* @param schema
* Schema to reorder; If {@code null} or not a {@code UNION}, then it is returned unmodified.
* @param defaultValue
* Default value to match with the union
*
* @return A schema modified so the first branch matches the type of {@code defaultValue}; otherwise, {@code schema} is returned
* unmodified.
*/
protected Schema reorderUnionToMatchDefaultType(Schema schema, JsonNode defaultValue) {
if (schema == null || defaultValue == null || schema.getType() != Type.UNION) {
return schema;
}
List<Schema> types = new ArrayList<>(schema.getTypes());
Integer matchingIndex = null;
if (defaultValue.isArray()) {
matchingIndex = schema.getIndexNamed(Type.ARRAY.getName());
} else if (defaultValue.isObject()) {
matchingIndex = schema.getIndexNamed(Type.MAP.getName());
if (matchingIndex == null) {
// search for a record
for (int i = 0; i < types.size(); i++) {
if (types.get(i).getType() == Type.RECORD) {
matchingIndex = i;
break;
}
}
}
} else if (defaultValue.isBoolean()) {
matchingIndex = schema.getIndexNamed(Type.BOOLEAN.getName());
} else if (defaultValue.isNull()) {
matchingIndex = schema.getIndexNamed(Type.NULL.getName());
} else if (defaultValue.isBinary()) {
matchingIndex = schema.getIndexNamed(Type.BYTES.getName());
} else if (defaultValue.isFloatingPointNumber()) {
matchingIndex = schema.getIndexNamed(Type.DOUBLE.getName());
if (matchingIndex == null) {
matchingIndex = schema.getIndexNamed(Type.FLOAT.getName());
}
} else if (defaultValue.isIntegralNumber()) {
matchingIndex = schema.getIndexNamed(Type.LONG.getName());
if (matchingIndex == null) {
matchingIndex = schema.getIndexNamed(Type.INT.getName());
}
} else if (defaultValue.isTextual()) {
matchingIndex = schema.getIndexNamed(Type.STRING.getName());
if (matchingIndex == null) {
// search for an enum
for (int i = 0; i < types.size(); i++) {
if (types.get(i).getType() == Type.ENUM) {
matchingIndex = i;
break;
}
}
}
}
if (matchingIndex != null) {
types.add(0, types.remove((int)matchingIndex));
Map<String, JsonNode> jsonProps = schema.getJsonProps();
schema = Schema.createUnion(types);
// copy any properties over
for (String property : jsonProps.keySet()) {
schema.addProp(property, jsonProps.get(property));
}
}
return schema;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.fasterxml.jackson.dataformat.avro.interop.annotations;

import com.fasterxml.jackson.dataformat.avro.interop.ApacheAvroInteropUtil;

import org.apache.avro.Schema;
import org.apache.avro.reflect.AvroDefault;
import org.junit.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class AvroDefaultTest {
static class RecordWithDefaults {
@AvroDefault("\"Test Field\"")
public String stringField;
@AvroDefault("1234")
public Integer intField;
@AvroDefault("true")
public Integer booleanField;
}

@Test
public void testUnionBooleanDefault() {
Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class);
Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class);
//
assertThat(jacksonSchema.getField("booleanField").defaultValue()).isEqualTo(apacheSchema.getField("booleanField").defaultValue());
}

@Test
public void testUnionIntegerDefault() {
Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class);
Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class);
//
assertThat(jacksonSchema.getField("intField").defaultValue()).isEqualTo(apacheSchema.getField("intField").defaultValue());
}

@Test
public void testUnionStringDefault() {
Schema apacheSchema = ApacheAvroInteropUtil.getApacheSchema(RecordWithDefaults.class);
Schema jacksonSchema = ApacheAvroInteropUtil.getJacksonSchema(RecordWithDefaults.class);
//
assertThat(jacksonSchema.getField("stringField").defaultValue()).isEqualTo(apacheSchema.getField("stringField").defaultValue());
}
}