From 62570b41aa13bba0057a10b05609918188244e31 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Tue, 8 Jun 2021 10:08:00 +0200 Subject: [PATCH 1/9] logicalType support for few date time types. --- .../avro/schema/DateTimeVisitor.java | 83 +++++++++++++++++++ .../avro/schema/VisitorFormatWrapperImpl.java | 42 ++++++++-- 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DateTimeVisitor.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DateTimeVisitor.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DateTimeVisitor.java new file mode 100644 index 000000000..27a6beba7 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/DateTimeVisitor.java @@ -0,0 +1,83 @@ +package com.fasterxml.jackson.dataformat.avro.schema; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; + +public class DateTimeVisitor extends JsonIntegerFormatVisitor.Base + implements SchemaBuilder { + + protected JsonParser.NumberType _type; + protected JavaType _hint; + + public DateTimeVisitor() { + } + + public DateTimeVisitor(JavaType typeHint) { + _hint = typeHint; + } + + @Override + public void numberType(JsonParser.NumberType type) { + _type = type; + } + + @Override + public Schema builtAvroSchema() { + if (_type == null) { + throw new IllegalStateException("No number type indicated"); + } + + Schema schema = AvroSchemaHelper.numericAvroSchema(_type); + if (_hint != null) { + String logicalType = logicalType(_hint); + if (logicalType != null) { + schema.addProp(LogicalType.LOGICAL_TYPE_PROP, logicalType); + } else { + schema.addProp(AvroSchemaHelper.AVRO_SCHEMA_PROP_CLASS, AvroSchemaHelper.getTypeId(_hint)); + } + } + return schema; + } + + private String logicalType(JavaType hint) { + Class clazz = hint.getRawClass(); + + if (OffsetDateTime.class.isAssignableFrom(clazz)) { + return TIMESTAMP_MILLIS; + } + if (ZonedDateTime.class.isAssignableFrom(clazz)) { + return TIMESTAMP_MILLIS; + } + if (Instant.class.isAssignableFrom(clazz)) { + return TIMESTAMP_MILLIS; + } + + if (LocalDate.class.isAssignableFrom(clazz)) { + return DATE; + } + if (LocalTime.class.isAssignableFrom(clazz)) { + return TIME_MILLIS; + } + if (LocalDateTime.class.isAssignableFrom(clazz)) { + return LOCAL_TIMESTAMP_MILLIS; + } + + return null; + } + + private static final String DATE = "date"; + private static final String TIME_MILLIS = "time-millis"; + private static final String TIMESTAMP_MILLIS = "timestamp-millis"; + private static final String LOCAL_TIMESTAMP_MILLIS = "local-timestamp-millis"; + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java index c180d3732..be2d8c164 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java @@ -1,21 +1,30 @@ package com.fasterxml.jackson.dataformat.avro.schema; -import org.apache.avro.Schema; - import com.fasterxml.jackson.core.JsonGenerator; - import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; -import com.fasterxml.jackson.databind.jsonFormatVisitors.*; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonBooleanFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNullFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; import com.fasterxml.jackson.dataformat.avro.AvroSchema; +import org.apache.avro.Schema; + +import java.time.temporal.Temporal; public class VisitorFormatWrapperImpl implements JsonFormatVisitorWrapper { protected SerializerProvider _provider; - + protected final DefinedSchemas _schemas; /** @@ -28,7 +37,7 @@ public class VisitorFormatWrapperImpl * Schema for simple types that do not need a visitor. */ protected Schema _valueSchema; - + /* /********************************************************************** /* Construction @@ -39,7 +48,7 @@ public VisitorFormatWrapperImpl(DefinedSchemas schemas, SerializerProvider p) { _schemas = schemas; _provider = p; } - + @Override public SerializerProvider getProvider() { return _provider; @@ -67,7 +76,7 @@ public Schema getAvroSchema() { } return _builder.builtAvroSchema(); } - + /* /********************************************************************** /* Callbacks @@ -97,7 +106,7 @@ public JsonMapFormatVisitor expectMapFormat(JavaType mapType) { _builder = v; return v; } - + @Override public JsonArrayFormatVisitor expectArrayFormat(final JavaType convertedType) { // 22-Mar-2016, tatu: Actually we can detect byte[] quite easily here can't we? @@ -148,6 +157,13 @@ public JsonIntegerFormatVisitor expectIntegerFormat(JavaType type) { _valueSchema = s; return null; } + + if (_isDateTimeType(type)) { + DateTimeVisitor v = new DateTimeVisitor(type); + _builder = v; + return v; + } + IntegerVisitor v = new IntegerVisitor(type); _builder = v; return v; @@ -183,7 +199,15 @@ public JsonAnyFormatVisitor expectAnyFormat(JavaType convertedType) throws JsonM protected T _throwUnsupported() { return _throwUnsupported("Format variation not supported"); } + protected T _throwUnsupported(String msg) { throw new UnsupportedOperationException(msg); } + + private boolean _isDateTimeType(JavaType type) { + if (Temporal.class.isAssignableFrom(type.getRawClass())) { + return true; + } + return false; + } } From 1bd02bae3152ed3cbca9a1a073bdecbe3717e117 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Tue, 8 Jun 2021 11:53:29 +0200 Subject: [PATCH 2/9] Support to serialize and de-serialize java.time types into Avro type and logicalType. --- .../avro/jsr310/AvroJavaTimeModule.java | 61 ++++++++ .../jsr310/deser/AvroInstantDeserializer.java | 74 +++++++++ .../deser/AvroLocalDateDeserializer.java | 53 +++++++ .../deser/AvroLocalDateTimeDeserializer.java | 57 +++++++ .../deser/AvroLocalTimeDeserializer.java | 56 +++++++ .../jsr310/ser/AvroInstantSerializer.java | 76 +++++++++ .../jsr310/ser/AvroLocalDateSerializer.java | 56 +++++++ .../ser/AvroLocalDateTimeSerializer.java | 60 +++++++ .../jsr310/ser/AvroLocalTimeSerializer.java | 58 +++++++ ...AvroJavaTimeModule_schemaCreationTest.java | 75 +++++++++ ...serialization_and_deserializationTest.java | 147 ++++++++++++++++++ 11 files changed, 773 insertions(+) create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java create mode 100644 avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java create mode 100644 avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java new file mode 100644 index 000000000..e778e9819 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java @@ -0,0 +1,61 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.json.PackageVersion; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroInstantDeserializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateDeserializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateTimeDeserializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalTimeDeserializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroInstantSerializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateSerializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateTimeSerializer; +import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalTimeSerializer; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; + +/** + * A module that installs a collection of serializers and deserializers for java.time classes. + */ +public class AvroJavaTimeModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + + public AvroJavaTimeModule() { + super(PackageVersion.VERSION); + + addSerializer(Instant.class, AvroInstantSerializer.INSTANT); + addSerializer(OffsetDateTime.class, AvroInstantSerializer.OFFSET_DATE_TIME); + addSerializer(ZonedDateTime.class, AvroInstantSerializer.ZONED_DATE_TIME); + addSerializer(LocalDateTime.class, AvroLocalDateTimeSerializer.INSTANCE); + addSerializer(LocalDate.class, AvroLocalDateSerializer.INSTANCE); + addSerializer(LocalTime.class, AvroLocalTimeSerializer.INSTANCE); + + addDeserializer(Instant.class, AvroInstantDeserializer.INSTANT); + addDeserializer(OffsetDateTime.class, AvroInstantDeserializer.OFFSET_DATE_TIME); + addDeserializer(ZonedDateTime.class, AvroInstantDeserializer.ZONED_DATE_TIME); + addDeserializer(LocalDateTime.class, AvroLocalDateTimeDeserializer.INSTANCE); + addDeserializer(LocalDate.class, AvroLocalDateDeserializer.INSTANCE); + addDeserializer(LocalTime.class, AvroLocalTimeDeserializer.INSTANCE); + } + + @Override + public String getModuleName() { + return getClass().getName(); + } + + @Override + public Version version() { + return PackageVersion.VERSION; + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + } +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java new file mode 100644 index 000000000..410f46043 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java @@ -0,0 +1,74 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; +import java.util.function.BiFunction; + +/** + * Deserializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) from an integer value. + * + * Deserialized value represents an instant on the global timeline, independent of a particular time zone or + * calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC. + * Time zone information is lost at serialization. Time zone data types receives time zone from deserialization context. + * + * Deserialization from string is not supported. + * + * @param The type of a instant class that can be deserialized. + */ +public class AvroInstantDeserializer extends StdScalarDeserializer { + + private static final long serialVersionUID = 1L; + + public static final AvroInstantDeserializer INSTANT = + new AvroInstantDeserializer<>(Instant.class, (instant, zoneID) -> instant); + + public static final AvroInstantDeserializer OFFSET_DATE_TIME = + new AvroInstantDeserializer<>(OffsetDateTime.class, OffsetDateTime::ofInstant); + + public static final AvroInstantDeserializer ZONED_DATE_TIME = + new AvroInstantDeserializer<>(ZonedDateTime.class, ZonedDateTime::ofInstant); + + protected final BiFunction fromInstant; + + protected AvroInstantDeserializer(Class t, BiFunction fromInstant) { + super(t); + this.fromInstant = fromInstant; + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { + final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized(); + switch (p.getCurrentToken()) { + case VALUE_NUMBER_INT: + return fromLong(p.getLongValue(), defaultZoneId); + default: + try { + return (T) context.handleUnexpectedToken(_valueClass, p); + } catch (JsonMappingException e) { + throw e; + } catch (IOException e) { + throw JsonMappingException.fromUnexpectedIOE(e); + } + } + } + + private T fromLong(long longValue, ZoneId defaultZoneId) { + /** + * Number of milliseconds, independent of a particular time zone or calendar, + * from 1 January 1970 00:00:00.000 UTC. + */ + return fromInstant.apply(Instant.ofEpochMilli(longValue), defaultZoneId); + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java new file mode 100644 index 000000000..08ab86e07 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java @@ -0,0 +1,53 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Deserializer for {@link LocalDate} from and integer value. + * + * Deserialized value represents number of days from the unix epoch, 1 January 1970. + * + * Deserialization from string is not supported. + */ +public class AvroLocalDateDeserializer extends StdScalarDeserializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalDateDeserializer INSTANCE = new AvroLocalDateDeserializer(); + + protected AvroLocalDateDeserializer() { + super(LocalDate.class); + } + + @SuppressWarnings("unchecked") + @Override + public LocalDate deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { + switch (p.getCurrentToken()) { + case VALUE_NUMBER_INT: + return fromLong(p.getLongValue()); + default: + try { + return (LocalDate) context.handleUnexpectedToken(_valueClass, p); + } catch (JsonMappingException e) { + throw e; + } catch (IOException e) { + throw JsonMappingException.fromUnexpectedIOE(e); + } + } + } + + private LocalDate fromLong(long longValue) { + /** + * Number of days from the unix epoch, 1 January 1970.. + */ + return LocalDate.ofEpochDay(longValue); + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java new file mode 100644 index 000000000..2a1bef4a0 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java @@ -0,0 +1,57 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * Deserializer for {@link LocalDateTime} from an integer value. + * + * Deserialized value represents timestamp in a local timezone, regardless of what specific time zone + * is considered local, with a precision of one millisecond from 1 January 1970 00:00:00.000. + * + * Deserialization from string is not supported. + */ +public class AvroLocalDateTimeDeserializer extends StdScalarDeserializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalDateTimeDeserializer INSTANCE = new AvroLocalDateTimeDeserializer(); + + protected AvroLocalDateTimeDeserializer() { + super(LocalDateTime.class); + } + + @SuppressWarnings("unchecked") + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { + switch (p.getCurrentToken()) { + case VALUE_NUMBER_INT: + return fromLong(p.getLongValue()); + default: + try { + return (LocalDateTime) context.handleUnexpectedToken(_valueClass, p); + } catch (JsonMappingException e) { + throw e; + } catch (IOException e) { + throw JsonMappingException.fromUnexpectedIOE(e); + } + } + } + + private LocalDateTime fromLong(long longValue) { + /** + * Number of milliseconds in a local timezone, regardless of what specific time zone is considered local, + * from 1 January 1970 00:00:00.000. + */ + return LocalDateTime.ofInstant(Instant.ofEpochMilli(longValue), ZoneOffset.ofTotalSeconds(0)); + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java new file mode 100644 index 000000000..1b90fbb9a --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * Deserializer for {@link LocalTime} from an integer value. + * + * Deserialized value represents time of day, with no reference to a particular calendar, + * time zone or date, where the int stores the number of milliseconds after midnight, 00:00:00.000. + * + * Deserialization from string is not supported. + */ +public class AvroLocalTimeDeserializer extends StdScalarDeserializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalTimeDeserializer INSTANCE = new AvroLocalTimeDeserializer(); + + protected AvroLocalTimeDeserializer() { + super(LocalDateTime.class); + } + + @SuppressWarnings("unchecked") + @Override + public LocalTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { + switch (p.getCurrentToken()) { + case VALUE_NUMBER_INT: + return fromLong(p.getLongValue()); + default: + try { + return (LocalTime) context.handleUnexpectedToken(_valueClass, p); + } catch (JsonMappingException e) { + throw e; + } catch (IOException e) { + throw JsonMappingException.fromUnexpectedIOE(e); + } + } + } + + private LocalTime fromLong(long longValue) { + /** + * Number of milliseconds, with no reference to a particular calendar, time zone or date, after + * midnight, 00:00:00.000. + */ + return LocalTime.ofNanoOfDay(longValue * 1000_000L); + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java new file mode 100644 index 000000000..eedfe8a72 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java @@ -0,0 +1,76 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; +import java.util.function.Function; + +/** + * Serializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) into long value. + * + * Serialized value represents an instant on the global timeline, independent of a particular time zone or + * calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC. + * Please note that time zone information gets lost in this process. Upon reading a value back, we can only + * reconstruct the instant, but not the original representation. + * + * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce + * Avro schema with type long with logicalType timestamp-millis: + * { + * "type" : "long", + * "logicalType" : "timestamp-millis" + * } + * + * {@link AvroInstantSerializer} does not support serialization to string. + * + * @param The type of a instant class that can be serialized. + */ +public class AvroInstantSerializer extends StdScalarSerializer { + + private static final long serialVersionUID = 1L; + + public static final AvroInstantSerializer INSTANT = + new AvroInstantSerializer<>(Instant.class, Function.identity()); + + public static final AvroInstantSerializer OFFSET_DATE_TIME = + new AvroInstantSerializer<>(OffsetDateTime.class, OffsetDateTime::toInstant); + + public static final AvroInstantSerializer ZONED_DATE_TIME = + new AvroInstantSerializer<>(ZonedDateTime.class, ZonedDateTime::toInstant); + + private final Function getInstant; + + protected AvroInstantSerializer(Class t, Function getInstant) { + super(t); + this.getInstant = getInstant; + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException { + /** + * Number of milliseconds, independent of a particular time zone or calendar, + * from 1 January 1970 00:00:00.000 UTC. + */ + final Instant instant = getInstant.apply(value); + gen.writeNumber(instant.toEpochMilli()); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.LONG); + } + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java new file mode 100644 index 000000000..07531c865 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java @@ -0,0 +1,56 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * Serializer for {@link LocalDate} into int value. + * + * Serialized value represents number of days from the unix epoch, 1 January 1970 with no reference + * to a particular time zone or time of day. + * + * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce + * Avro schema with type int with logicalType date: + * { + * "type" : "int", + * "logicalType" : "date" + * } + * + * Serialization to string is not supported. + */ +public class AvroLocalDateSerializer extends StdScalarSerializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalDateSerializer INSTANCE = new AvroLocalDateSerializer(); + + protected AvroLocalDateSerializer() { + super(LocalDate.class); + } + + @Override + public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException { + /** + * Number of days from the unix epoch, 1 January 1970. + */ + gen.writeNumber(value.toEpochDay()); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.INT); + } + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java new file mode 100644 index 000000000..3b00f2b46 --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java @@ -0,0 +1,60 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +/** + * Serializer for {@link LocalDateTime} into long value + * + * Serialized value represents timestamp in a local timezone, regardless of what specific time zone + * is considered local, with a precision of one millisecond from 1 January 1970 00:00:00.000. + * + * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce + * Avro schema with type long with logicalType local-timestamp-millis: + * { + * "type" : "long", + * "logicalType" : "local-timestamp-millis" + * } + * + * Serialization to string is not supported. + */ +public class AvroLocalDateTimeSerializer extends StdScalarSerializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalDateTimeSerializer INSTANCE = new AvroLocalDateTimeSerializer(); + + protected AvroLocalDateTimeSerializer() { + super(LocalDateTime.class); + } + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException { + /** + * Number of milliseconds in a local timezone, regardless of what specific time zone is considered local, + * from 1 January 1970 00:00:00.000. + */ + final Instant instant = value.toInstant(ZoneOffset.ofTotalSeconds(0)); + gen.writeNumber(instant.toEpochMilli()); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.LONG); + } + } + +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java new file mode 100644 index 000000000..db60275af --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java @@ -0,0 +1,58 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.ser; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor; +import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; + +import java.io.IOException; +import java.time.LocalTime; + +/** + * Serializer for {@link LocalTime} into int value. + * + * Serialized value represents time of day, with no reference to a particular calendar, + * time zone or date, where the int stores the number of milliseconds after midnight, 00:00:00.000. + * + * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce + * Avro schema with type int with logicalType time-millis: + * { + * "type" : "int", + * "logicalType" : "time-millis" + * } + * + * Serialization to string is not supported. + */ +public class AvroLocalTimeSerializer extends StdScalarSerializer { + + private static final long serialVersionUID = 1L; + + public static final AvroLocalTimeSerializer INSTANCE = new AvroLocalTimeSerializer(); + + protected AvroLocalTimeSerializer() { + super(LocalTime.class); + } + + @Override + public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider provider) throws IOException { + /** + * Number of milliseconds, with no reference to a particular calendar, time zone or date, after + * midnight, 00:00:00.000. + */ + long milliOfDay = value.toNanoOfDay() / 1000_000L; + gen.writeNumber(milliOfDay); + } + + @Override + public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException { + JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint); + if (v2 != null) { + v2.numberType(JsonParser.NumberType.INT); + } + } + +} diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java new file mode 100644 index 000000000..2aa649a9e --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java @@ -0,0 +1,75 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator; +import org.apache.avro.LogicalType; +import org.apache.avro.Schema; +import org.apache.avro.specific.SpecificData; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(Parameterized.class) +public class AvroJavaTimeModule_schemaCreationTest { + + @Parameter(0) + public Class testClass; + + @Parameter(1) + public Schema.Type expectedType; + + @Parameter(2) + public String expectedLogicalType; + + @Parameters(name = "With {0}") + public static Collection testData() { + return Arrays.asList(new Object[][]{ + // Java type | expected Avro type | expected logicalType + {Instant.class, Schema.Type.LONG, "timestamp-millis"}, + {OffsetDateTime.class, Schema.Type.LONG, "timestamp-millis"}, + {ZonedDateTime.class, Schema.Type.LONG, "timestamp-millis"}, + {LocalDateTime.class, Schema.Type.LONG, "local-timestamp-millis"}, + {LocalDate.class, Schema.Type.INT, "date"}, + {LocalTime.class, Schema.Type.INT, "time-millis"}, + }); + } + + @Test + public void testSchemaCreation() throws JsonMappingException { + // GIVEN + AvroMapper mapper = AvroMapper.builder() + .addModules(new AvroJavaTimeModule()) + .build(); + AvroSchemaGenerator gen = new AvroSchemaGenerator(); + + // WHEN + mapper.acceptJsonFormatVisitor(testClass, gen); + Schema actualSchema = gen.getGeneratedSchema().getAvroSchema(); + + System.out.println(testClass.getName() + " schema:\n" + actualSchema.toString(true)); + + // THEN + assertThat(actualSchema.getType()).isEqualTo(expectedType); + assertThat(actualSchema.getProp(LogicalType.LOGICAL_TYPE_PROP)).isEqualTo(expectedLogicalType); + /** + * Having logicalType and java-class is not valid according to + * {@link LogicalType#validate(Schema)} + */ + assertThat(actualSchema.getProp(SpecificData.CLASS_PROP)).isNull(); + } + +} diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java new file mode 100644 index 000000000..463c21620 --- /dev/null +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java @@ -0,0 +1,147 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310; + +import com.fasterxml.jackson.dataformat.avro.AvroMapper; +import com.fasterxml.jackson.dataformat.avro.AvroSchema; +import org.junit.Test; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AvroJavaTimeModule_serialization_and_deserializationTest { + + static final String SCHEMA_LONG_AND_TIMESTAMP_MILLIS = "{" + + " \"type\": \"long\"," + + " \"logicalType\": \"timestamp-millis\"" + + "}"; + + static final String SCHEMA_LONG_AND_LOCAL_TIMESTAMP_MILLIS = "{" + + " \"type\": \"long\"," + + " \"logicalType\": \"local-timestamp-millis\"" + + "}"; + + static final String SCHEMA_INT_AND_TIME_MILLIS = "{" + + " \"type\": \"int\"," + + " \"logicalType\": \"time-millis\"" + + "}"; + + static final String SCHEMA_INT_AND_DATE = "{" + + " \"type\": \"int\"," + + " \"logicalType\": \"date\"" + + "}"; + + private static AvroMapper newAvroMapper() { + return AvroMapper.builder() + .addModules(new AvroJavaTimeModule()) + .build(); + } + + @Test + public void testWithInstant_millis() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_LONG_AND_TIMESTAMP_MILLIS); + + Instant serializedInstant = Instant.ofEpochSecond(930303030, 333_222_111); + Instant expectedInstant = Instant.ofEpochSecond(930303030, 333_000_000); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(serializedInstant); + Instant deserInstant = mapper.readerFor(Instant.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserInstant).isEqualTo(expectedInstant); + } + + @Test + public void testWithOffsetDateTime_millis() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_LONG_AND_TIMESTAMP_MILLIS); + + OffsetDateTime serializedOffsetDateTime = OffsetDateTime.of(2021, 6, 6, 12, 00, 30, 333_222_111, ZoneOffset.ofHours(2)); + OffsetDateTime expectedOffsetDateTime = OffsetDateTime.of(2021, 6, 6, 12, 00, 30, 333_000_000, ZoneOffset.ofHours(2)); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(serializedOffsetDateTime); + OffsetDateTime deserOffsetDateTime = mapper.readerFor(OffsetDateTime.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserOffsetDateTime.toInstant()).isEqualTo(expectedOffsetDateTime.toInstant()); + } + + @Test + public void testWithZonedDateTime_millis() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_LONG_AND_TIMESTAMP_MILLIS); + + ZonedDateTime serializedZonedDateTime = ZonedDateTime.of(2021, 6, 6, 12, 00, 30, 333_222_111, ZoneOffset.ofHours(2)); + ZonedDateTime expectedZonedDateTime = ZonedDateTime.of(2021, 6, 6, 12, 00, 30, 333_000_000, ZoneOffset.ofHours(2)); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(serializedZonedDateTime); + ZonedDateTime deserZonedDateTime = mapper.readerFor(ZonedDateTime.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserZonedDateTime.toInstant()).isEqualTo(expectedZonedDateTime.toInstant()); + } + + @Test + public void testWithLocalDateTime_millis() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_LONG_AND_LOCAL_TIMESTAMP_MILLIS); + + LocalDateTime serializedLocalDateTime = LocalDateTime.of(2021, 6, 6, 12, 0, 30, 333_222_111); + LocalDateTime expectedLocalDateTime = LocalDateTime.of(2021, 6, 6, 12, 0, 30, 333_000_000); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(serializedLocalDateTime); + LocalDateTime deserLocalDateTime = mapper.readerFor(LocalDateTime.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserLocalDateTime).isEqualTo(expectedLocalDateTime); + } + + @Test + public void testWithLocalDate() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_INT_AND_DATE); + + LocalDate expectedLocalDate = LocalDate.of(2021, 6, 7); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(expectedLocalDate); + LocalDate deserLocalDate = mapper.readerFor(LocalDate.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserLocalDate).isEqualTo(expectedLocalDate); + } + + @Test + public void testWithLocalTime_millis() throws IOException { + // GIVEN + AvroMapper mapper = newAvroMapper(); + AvroSchema schema = mapper.schemaFrom(SCHEMA_INT_AND_TIME_MILLIS); + + LocalTime serializedLocalTime = LocalTime.of(23, 6, 6, 333_222_111); + LocalTime expectedLocalTime = LocalTime.of(23, 6, 6, 333_000_000); + + // WHEN + byte[] serialized = mapper.writer(schema).writeValueAsBytes(serializedLocalTime); + LocalTime deserLocalTime = mapper.readerFor(LocalTime.class).with(schema).readValue(serialized); + + // THEN + assertThat(deserLocalTime).isEqualTo(expectedLocalTime); + } + +} From 8f52e52b297607da542ff83c46059a666410db87 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Mon, 14 Jun 2021 16:14:57 +0200 Subject: [PATCH 3/9] Base deserializer and try-catch block removed, no need to try to wrap anything, expected pattern is to let exception pass through if any. --- .../jsr310/deser/AvroInstantDeserializer.java | 32 +++-------------- .../deser/AvroJavaTimeDeserializerBase.java | 34 +++++++++++++++++++ .../deser/AvroLocalDateDeserializer.java | 28 ++------------- .../deser/AvroLocalDateTimeDeserializer.java | 28 ++------------- .../deser/AvroLocalTimeDeserializer.java | 31 +++-------------- 5 files changed, 48 insertions(+), 105 deletions(-) create mode 100644 avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java index 410f46043..9a2d6e986 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroInstantDeserializer.java @@ -1,12 +1,5 @@ package com.fasterxml.jackson.dataformat.avro.jsr310.deser; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; - -import java.io.IOException; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneId; @@ -25,7 +18,7 @@ * * @param The type of a instant class that can be deserialized. */ -public class AvroInstantDeserializer extends StdScalarDeserializer { +public class AvroInstantDeserializer extends AvroJavaTimeDeserializerBase { private static final long serialVersionUID = 1L; @@ -40,30 +33,13 @@ public class AvroInstantDeserializer extends StdScalarDeseri protected final BiFunction fromInstant; - protected AvroInstantDeserializer(Class t, BiFunction fromInstant) { - super(t); + protected AvroInstantDeserializer(Class supportedType, BiFunction fromInstant) { + super(supportedType); this.fromInstant = fromInstant; } - @SuppressWarnings("unchecked") @Override - public T deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { - final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized(); - switch (p.getCurrentToken()) { - case VALUE_NUMBER_INT: - return fromLong(p.getLongValue(), defaultZoneId); - default: - try { - return (T) context.handleUnexpectedToken(_valueClass, p); - } catch (JsonMappingException e) { - throw e; - } catch (IOException e) { - throw JsonMappingException.fromUnexpectedIOE(e); - } - } - } - - private T fromLong(long longValue, ZoneId defaultZoneId) { + protected T fromLong(long longValue, ZoneId defaultZoneId) { /** * Number of milliseconds, independent of a particular time zone or calendar, * from 1 January 1970 00:00:00.000 UTC. diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java new file mode 100644 index 000000000..f80908cbf --- /dev/null +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java @@ -0,0 +1,34 @@ +package com.fasterxml.jackson.dataformat.avro.jsr310.deser; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.type.LogicalType; + +import java.io.IOException; +import java.time.ZoneId; + +public abstract class AvroJavaTimeDeserializerBase extends StdScalarDeserializer { + + protected AvroJavaTimeDeserializerBase(Class supportedType) { + super(supportedType); + } + + @Override + public LogicalType logicalType() { + return LogicalType.DateTime; + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser p, DeserializationContext context) throws IOException { + final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized(); + switch (p.getCurrentToken()) { + case VALUE_NUMBER_INT: + return fromLong(p.getLongValue(), defaultZoneId); + } + return (T) context.handleUnexpectedToken(_valueClass, p); + } + + protected abstract T fromLong(long longValue, ZoneId defaultZoneId); +} diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java index 08ab86e07..21fd7c20c 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateDeserializer.java @@ -1,13 +1,7 @@ package com.fasterxml.jackson.dataformat.avro.jsr310.deser; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; - -import java.io.IOException; import java.time.LocalDate; +import java.time.ZoneId; /** * Deserializer for {@link LocalDate} from and integer value. @@ -16,7 +10,7 @@ * * Deserialization from string is not supported. */ -public class AvroLocalDateDeserializer extends StdScalarDeserializer { +public class AvroLocalDateDeserializer extends AvroJavaTimeDeserializerBase { private static final long serialVersionUID = 1L; @@ -26,24 +20,8 @@ protected AvroLocalDateDeserializer() { super(LocalDate.class); } - @SuppressWarnings("unchecked") @Override - public LocalDate deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { - switch (p.getCurrentToken()) { - case VALUE_NUMBER_INT: - return fromLong(p.getLongValue()); - default: - try { - return (LocalDate) context.handleUnexpectedToken(_valueClass, p); - } catch (JsonMappingException e) { - throw e; - } catch (IOException e) { - throw JsonMappingException.fromUnexpectedIOE(e); - } - } - } - - private LocalDate fromLong(long longValue) { + protected LocalDate fromLong(long longValue, ZoneId defaultZoneId) { /** * Number of days from the unix epoch, 1 January 1970.. */ diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java index 2a1bef4a0..e2318fae2 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalDateTimeDeserializer.java @@ -1,14 +1,8 @@ package com.fasterxml.jackson.dataformat.avro.jsr310.deser; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; - -import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; /** @@ -19,7 +13,7 @@ * * Deserialization from string is not supported. */ -public class AvroLocalDateTimeDeserializer extends StdScalarDeserializer { +public class AvroLocalDateTimeDeserializer extends AvroJavaTimeDeserializerBase { private static final long serialVersionUID = 1L; @@ -29,24 +23,8 @@ protected AvroLocalDateTimeDeserializer() { super(LocalDateTime.class); } - @SuppressWarnings("unchecked") @Override - public LocalDateTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { - switch (p.getCurrentToken()) { - case VALUE_NUMBER_INT: - return fromLong(p.getLongValue()); - default: - try { - return (LocalDateTime) context.handleUnexpectedToken(_valueClass, p); - } catch (JsonMappingException e) { - throw e; - } catch (IOException e) { - throw JsonMappingException.fromUnexpectedIOE(e); - } - } - } - - private LocalDateTime fromLong(long longValue) { + protected LocalDateTime fromLong(long longValue, ZoneId defaultZoneId) { /** * Number of milliseconds in a local timezone, regardless of what specific time zone is considered local, * from 1 January 1970 00:00:00.000. diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java index 1b90fbb9a..1d98a3890 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroLocalTimeDeserializer.java @@ -1,14 +1,7 @@ package com.fasterxml.jackson.dataformat.avro.jsr310.deser; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; - -import java.io.IOException; -import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.ZoneId; /** * Deserializer for {@link LocalTime} from an integer value. @@ -18,34 +11,18 @@ * * Deserialization from string is not supported. */ -public class AvroLocalTimeDeserializer extends StdScalarDeserializer { +public class AvroLocalTimeDeserializer extends AvroJavaTimeDeserializerBase { private static final long serialVersionUID = 1L; public static final AvroLocalTimeDeserializer INSTANCE = new AvroLocalTimeDeserializer(); protected AvroLocalTimeDeserializer() { - super(LocalDateTime.class); + super(LocalTime.class); } - @SuppressWarnings("unchecked") @Override - public LocalTime deserialize(JsonParser p, DeserializationContext context) throws IOException, JsonProcessingException { - switch (p.getCurrentToken()) { - case VALUE_NUMBER_INT: - return fromLong(p.getLongValue()); - default: - try { - return (LocalTime) context.handleUnexpectedToken(_valueClass, p); - } catch (JsonMappingException e) { - throw e; - } catch (IOException e) { - throw JsonMappingException.fromUnexpectedIOE(e); - } - } - } - - private LocalTime fromLong(long longValue) { + protected LocalTime fromLong(long longValue, ZoneId defaultZoneId) { /** * Number of milliseconds, with no reference to a particular calendar, time zone or date, after * midnight, 00:00:00.000. From ba763757ec66897887239f178a5564c54e3956a5 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Mon, 14 Jun 2021 20:16:17 +0200 Subject: [PATCH 4/9] Documentation added and corrected. --- avro/README.md | 30 +++++++++++++++++++ .../jsr310/ser/AvroInstantSerializer.java | 2 +- .../jsr310/ser/AvroLocalDateSerializer.java | 2 +- .../ser/AvroLocalDateTimeSerializer.java | 2 +- .../jsr310/ser/AvroLocalTimeSerializer.java | 2 +- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/avro/README.md b/avro/README.md index 6ffcb7a74..3db753921 100644 --- a/avro/README.md +++ b/avro/README.md @@ -111,6 +111,36 @@ byte[] avroData = mapper.writer(schema) and that's about it, for now. +## Java Time Support +Serialization and deserialization support for limited set of `java.time` classes to Avro with [logical type](http://avro.apache.org/docs/current/spec.html#Logical+Types) is provided by `AvroJavaTimeModule`. + +```java +AvroMapper mapper = AvroMapper.builder() + .addModules(new AvroJavaTimeModule()) + .build(); +``` + +#### Note +Please note that time zone information is at serialization. Serialized values represent point in time, +independent of a particular time zone or calendar. Upon reading a value back time instant is reconstructed but not the original time zone. + +#### Supported java.time types: + +Supported java.time types with Avro schema. + +| Type | Avro schema +| ------------------------------ | ------------- +| `java.time.OffsetDateTime` | `{"type": "long", "logicalType": "timestamp-millis"}` +| `java.time.ZonedDateTime` | `{"type": "long", "logicalType": "timestamp-millis"}` +| `java.time.Instant` | `{"type": "long", "logicalType": "timestamp-millis"}` +| `java.time.LocalDate` | `{"type": "int", "logicalType": "date"}` +| `java.time.LocalTime` | `{"type": "int", "logicalType": "time-millis"}` +| `java.time.LocalDateTime` | `{"type": "long", "logicalType": "local-timestamp-millis"}` + +#### Precision + +Avro supports milliseconds and microseconds previsions for date and time related logicalType(s). Only the milliseconds precision is supported. + ## Generating Avro Schema from POJO definition Ok but wait -- you do not have to START with an Avro Schema. This module can diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java index eedfe8a72..a816a37cd 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroInstantSerializer.java @@ -25,7 +25,7 @@ * reconstruct the instant, but not the original representation. * * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce - * Avro schema with type long with logicalType timestamp-millis: + * Avro schema with type long and logicalType timestamp-millis: * { * "type" : "long", * "logicalType" : "timestamp-millis" diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java index 07531c865..89d932344 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateSerializer.java @@ -19,7 +19,7 @@ * to a particular time zone or time of day. * * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce - * Avro schema with type int with logicalType date: + * Avro schema with type int and logicalType date: * { * "type" : "int", * "logicalType" : "date" diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java index 3b00f2b46..0c58c2d0f 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalDateTimeSerializer.java @@ -21,7 +21,7 @@ * is considered local, with a precision of one millisecond from 1 January 1970 00:00:00.000. * * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce - * Avro schema with type long with logicalType local-timestamp-millis: + * Avro schema with type long and logicalType local-timestamp-millis: * { * "type" : "long", * "logicalType" : "local-timestamp-millis" diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java index db60275af..5bc2485d2 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/ser/AvroLocalTimeSerializer.java @@ -19,7 +19,7 @@ * time zone or date, where the int stores the number of milliseconds after midnight, 00:00:00.000. * * Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce - * Avro schema with type int with logicalType time-millis: + * Avro schema with type int and logicalType time-millis: * { * "type" : "int", * "logicalType" : "time-millis" From 956f365729e8c03773a7931be43c678754f8cb31 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Thu, 17 Jun 2021 08:35:22 +0200 Subject: [PATCH 5/9] Use addModule() instead of addModules() --- avro/README.md | 2 +- .../avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java | 2 +- ...vroJavaTimeModule_serialization_and_deserializationTest.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/avro/README.md b/avro/README.md index 3db753921..0c5701106 100644 --- a/avro/README.md +++ b/avro/README.md @@ -116,7 +116,7 @@ Serialization and deserialization support for limited set of `java.time` classes ```java AvroMapper mapper = AvroMapper.builder() - .addModules(new AvroJavaTimeModule()) + .addModule(new AvroJavaTimeModule()) .build(); ``` diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java index 2aa649a9e..b5edb2ef5 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_schemaCreationTest.java @@ -52,7 +52,7 @@ public static Collection testData() { public void testSchemaCreation() throws JsonMappingException { // GIVEN AvroMapper mapper = AvroMapper.builder() - .addModules(new AvroJavaTimeModule()) + .addModule(new AvroJavaTimeModule()) .build(); AvroSchemaGenerator gen = new AvroSchemaGenerator(); diff --git a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java index 463c21620..1e67bcdc2 100644 --- a/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java +++ b/avro/src/test/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule_serialization_and_deserializationTest.java @@ -39,7 +39,7 @@ public class AvroJavaTimeModule_serialization_and_deserializationTest { private static AvroMapper newAvroMapper() { return AvroMapper.builder() - .addModules(new AvroJavaTimeModule()) + .addModule(new AvroJavaTimeModule()) .build(); } From 170525f82136015a3b50a3816568c07ef76dfe37 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Thu, 17 Jun 2021 08:38:33 +0200 Subject: [PATCH 6/9] Use `com.fasterxml.jackson.dataformat.avro.PackageVersion` instead of 'com.fasterxml.jackson.core.json.PackageVersion' With correct parent constructor call version(), getModuleName() and setupModule() can be removed. --- .../avro/jsr310/AvroJavaTimeModule.java | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java index e778e9819..24d27f1c6 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java @@ -1,8 +1,7 @@ package com.fasterxml.jackson.dataformat.avro.jsr310; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.json.PackageVersion; import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.dataformat.avro.PackageVersion; import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroInstantDeserializer; import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateDeserializer; import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateTimeDeserializer; @@ -27,7 +26,7 @@ public class AvroJavaTimeModule extends SimpleModule { private static final long serialVersionUID = 1L; public AvroJavaTimeModule() { - super(PackageVersion.VERSION); + super(AvroJavaTimeModule.class.getName(), PackageVersion.VERSION); addSerializer(Instant.class, AvroInstantSerializer.INSTANT); addSerializer(OffsetDateTime.class, AvroInstantSerializer.OFFSET_DATE_TIME); @@ -44,18 +43,4 @@ public AvroJavaTimeModule() { addDeserializer(LocalTime.class, AvroLocalTimeDeserializer.INSTANCE); } - @Override - public String getModuleName() { - return getClass().getName(); - } - - @Override - public Version version() { - return PackageVersion.VERSION; - } - - @Override - public void setupModule(SetupContext context) { - super.setupModule(context); - } } From e02185d92502a17ef364907ef6fe3810990f9b83 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Sat, 26 Jun 2021 14:21:22 +0200 Subject: [PATCH 7/9] Better explanation Co-authored-by: Drew Stephens --- avro/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avro/README.md b/avro/README.md index 0c5701106..2f67615fc 100644 --- a/avro/README.md +++ b/avro/README.md @@ -139,7 +139,7 @@ Supported java.time types with Avro schema. #### Precision -Avro supports milliseconds and microseconds previsions for date and time related logicalType(s). Only the milliseconds precision is supported. +Avro supports milliseconds and microseconds precision for date and time related LogicalTypes, but this module only supports millisecond precision. ## Generating Avro Schema from POJO definition From 707f3a47d7c7b126a8f9c8f459cda3494a811498 Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Sun, 27 Jun 2021 07:54:40 +0200 Subject: [PATCH 8/9] Bit easier to understand code Co-authored-by: Drew Stephens --- .../jsr310/deser/AvroJavaTimeDeserializerBase.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java index f80908cbf..130f9a335 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/deser/AvroJavaTimeDeserializerBase.java @@ -8,6 +8,8 @@ import java.io.IOException; import java.time.ZoneId; +import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT; + public abstract class AvroJavaTimeDeserializerBase extends StdScalarDeserializer { protected AvroJavaTimeDeserializerBase(Class supportedType) { @@ -22,12 +24,12 @@ public LogicalType logicalType() { @SuppressWarnings("unchecked") @Override public T deserialize(JsonParser p, DeserializationContext context) throws IOException { - final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized(); - switch (p.getCurrentToken()) { - case VALUE_NUMBER_INT: - return fromLong(p.getLongValue(), defaultZoneId); + if (p.getCurrentToken() == VALUE_NUMBER_INT) { + final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized(); + return fromLong(p.getLongValue(), defaultZoneId); + } else { + return (T) context.handleUnexpectedToken(_valueClass, p); } - return (T) context.handleUnexpectedToken(_valueClass, p); } protected abstract T fromLong(long longValue, ZoneId defaultZoneId); From a05b4abd78aa007b1e54a0a84a753ec98129b63f Mon Sep 17 00:00:00 2001 From: Michal Foksa Date: Tue, 29 Jun 2021 07:25:01 +0200 Subject: [PATCH 9/9] More notes on AvroJavaTimeModule. --- avro/README.md | 6 +++++- .../jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/avro/README.md b/avro/README.md index 2f67615fc..2ceb76490 100644 --- a/avro/README.md +++ b/avro/README.md @@ -114,6 +114,10 @@ and that's about it, for now. ## Java Time Support Serialization and deserialization support for limited set of `java.time` classes to Avro with [logical type](http://avro.apache.org/docs/current/spec.html#Logical+Types) is provided by `AvroJavaTimeModule`. +This module is to be used either: +- Instead of Java 8 date/time module (`com.fasterxml.jackson.datatype.jsr310.JavaTimeModule`) or +- to override Java 8 date/time module and for that, module must be registered AFTER Java 8 date/time module (last registration wins). + ```java AvroMapper mapper = AvroMapper.builder() .addModule(new AvroJavaTimeModule()) @@ -121,7 +125,7 @@ AvroMapper mapper = AvroMapper.builder() ``` #### Note -Please note that time zone information is at serialization. Serialized values represent point in time, +Please note that time zone information is lost at serialization. Serialized values represent point in time, independent of a particular time zone or calendar. Upon reading a value back time instant is reconstructed but not the original time zone. #### Supported java.time types: diff --git a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java index 24d27f1c6..d901852d6 100644 --- a/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java +++ b/avro/src/main/java/com/fasterxml/jackson/dataformat/avro/jsr310/AvroJavaTimeModule.java @@ -20,6 +20,10 @@ /** * A module that installs a collection of serializers and deserializers for java.time classes. + * + * This module is to be used either: + * - Instead of Java 8 date/time module (com.fasterxml.jackson.datatype.jsr310.JavaTimeModule) or + * - to override Java 8 date/time module and for that, module must be registered AFTER Java 8 date/time module. */ public class AvroJavaTimeModule extends SimpleModule {