Skip to content

Commit c4ed624

Browse files
authored
[Avro] logicalType support for some java.time types. (#283)
Add * `logicalType` schema support for few date time types. * Support to serialize and de-serialize java.time types into Avro type and logicalType.
1 parent 4016f31 commit c4ed624

15 files changed

+857
-9
lines changed

avro/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,40 @@ byte[] avroData = mapper.writer(schema)
111111

112112
and that's about it, for now.
113113

114+
## Java Time Support
115+
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`.
116+
117+
This module is to be used either:
118+
- Instead of Java 8 date/time module (`com.fasterxml.jackson.datatype.jsr310.JavaTimeModule`) or
119+
- to override Java 8 date/time module and for that, module must be registered AFTER Java 8 date/time module (last registration wins).
120+
121+
```java
122+
AvroMapper mapper = AvroMapper.builder()
123+
.addModule(new AvroJavaTimeModule())
124+
.build();
125+
```
126+
127+
#### Note
128+
Please note that time zone information is lost at serialization. Serialized values represent point in time,
129+
independent of a particular time zone or calendar. Upon reading a value back time instant is reconstructed but not the original time zone.
130+
131+
#### Supported java.time types:
132+
133+
Supported java.time types with Avro schema.
134+
135+
| Type | Avro schema
136+
| ------------------------------ | -------------
137+
| `java.time.OffsetDateTime` | `{"type": "long", "logicalType": "timestamp-millis"}`
138+
| `java.time.ZonedDateTime` | `{"type": "long", "logicalType": "timestamp-millis"}`
139+
| `java.time.Instant` | `{"type": "long", "logicalType": "timestamp-millis"}`
140+
| `java.time.LocalDate` | `{"type": "int", "logicalType": "date"}`
141+
| `java.time.LocalTime` | `{"type": "int", "logicalType": "time-millis"}`
142+
| `java.time.LocalDateTime` | `{"type": "long", "logicalType": "local-timestamp-millis"}`
143+
144+
#### Precision
145+
146+
Avro supports milliseconds and microseconds precision for date and time related LogicalTypes, but this module only supports millisecond precision.
147+
114148
## Generating Avro Schema from POJO definition
115149

116150
Ok but wait -- you do not have to START with an Avro Schema. This module can
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310;
2+
3+
import com.fasterxml.jackson.databind.module.SimpleModule;
4+
import com.fasterxml.jackson.dataformat.avro.PackageVersion;
5+
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroInstantDeserializer;
6+
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateDeserializer;
7+
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalDateTimeDeserializer;
8+
import com.fasterxml.jackson.dataformat.avro.jsr310.deser.AvroLocalTimeDeserializer;
9+
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroInstantSerializer;
10+
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateSerializer;
11+
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalDateTimeSerializer;
12+
import com.fasterxml.jackson.dataformat.avro.jsr310.ser.AvroLocalTimeSerializer;
13+
14+
import java.time.Instant;
15+
import java.time.LocalDate;
16+
import java.time.LocalDateTime;
17+
import java.time.LocalTime;
18+
import java.time.OffsetDateTime;
19+
import java.time.ZonedDateTime;
20+
21+
/**
22+
* A module that installs a collection of serializers and deserializers for java.time classes.
23+
*
24+
* This module is to be used either:
25+
* - Instead of Java 8 date/time module (com.fasterxml.jackson.datatype.jsr310.JavaTimeModule) or
26+
* - to override Java 8 date/time module and for that, module must be registered AFTER Java 8 date/time module.
27+
*/
28+
public class AvroJavaTimeModule extends SimpleModule {
29+
30+
private static final long serialVersionUID = 1L;
31+
32+
public AvroJavaTimeModule() {
33+
super(AvroJavaTimeModule.class.getName(), PackageVersion.VERSION);
34+
35+
addSerializer(Instant.class, AvroInstantSerializer.INSTANT);
36+
addSerializer(OffsetDateTime.class, AvroInstantSerializer.OFFSET_DATE_TIME);
37+
addSerializer(ZonedDateTime.class, AvroInstantSerializer.ZONED_DATE_TIME);
38+
addSerializer(LocalDateTime.class, AvroLocalDateTimeSerializer.INSTANCE);
39+
addSerializer(LocalDate.class, AvroLocalDateSerializer.INSTANCE);
40+
addSerializer(LocalTime.class, AvroLocalTimeSerializer.INSTANCE);
41+
42+
addDeserializer(Instant.class, AvroInstantDeserializer.INSTANT);
43+
addDeserializer(OffsetDateTime.class, AvroInstantDeserializer.OFFSET_DATE_TIME);
44+
addDeserializer(ZonedDateTime.class, AvroInstantDeserializer.ZONED_DATE_TIME);
45+
addDeserializer(LocalDateTime.class, AvroLocalDateTimeDeserializer.INSTANCE);
46+
addDeserializer(LocalDate.class, AvroLocalDateDeserializer.INSTANCE);
47+
addDeserializer(LocalTime.class, AvroLocalTimeDeserializer.INSTANCE);
48+
}
49+
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;
2+
3+
import java.time.Instant;
4+
import java.time.OffsetDateTime;
5+
import java.time.ZoneId;
6+
import java.time.ZonedDateTime;
7+
import java.time.temporal.Temporal;
8+
import java.util.function.BiFunction;
9+
10+
/**
11+
* Deserializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) from an integer value.
12+
*
13+
* Deserialized value represents an instant on the global timeline, independent of a particular time zone or
14+
* calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC.
15+
* Time zone information is lost at serialization. Time zone data types receives time zone from deserialization context.
16+
*
17+
* Deserialization from string is not supported.
18+
*
19+
* @param <T> The type of a instant class that can be deserialized.
20+
*/
21+
public class AvroInstantDeserializer<T extends Temporal> extends AvroJavaTimeDeserializerBase <T> {
22+
23+
private static final long serialVersionUID = 1L;
24+
25+
public static final AvroInstantDeserializer<Instant> INSTANT =
26+
new AvroInstantDeserializer<>(Instant.class, (instant, zoneID) -> instant);
27+
28+
public static final AvroInstantDeserializer<OffsetDateTime> OFFSET_DATE_TIME =
29+
new AvroInstantDeserializer<>(OffsetDateTime.class, OffsetDateTime::ofInstant);
30+
31+
public static final AvroInstantDeserializer<ZonedDateTime> ZONED_DATE_TIME =
32+
new AvroInstantDeserializer<>(ZonedDateTime.class, ZonedDateTime::ofInstant);
33+
34+
protected final BiFunction<Instant, ZoneId, T> fromInstant;
35+
36+
protected AvroInstantDeserializer(Class<T> supportedType, BiFunction<Instant, ZoneId, T> fromInstant) {
37+
super(supportedType);
38+
this.fromInstant = fromInstant;
39+
}
40+
41+
@Override
42+
protected T fromLong(long longValue, ZoneId defaultZoneId) {
43+
/**
44+
* Number of milliseconds, independent of a particular time zone or calendar,
45+
* from 1 January 1970 00:00:00.000 UTC.
46+
*/
47+
return fromInstant.apply(Instant.ofEpochMilli(longValue), defaultZoneId);
48+
}
49+
50+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;
2+
3+
import com.fasterxml.jackson.core.JsonParser;
4+
import com.fasterxml.jackson.databind.DeserializationContext;
5+
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
6+
import com.fasterxml.jackson.databind.type.LogicalType;
7+
8+
import java.io.IOException;
9+
import java.time.ZoneId;
10+
11+
import static com.fasterxml.jackson.core.JsonToken.VALUE_NUMBER_INT;
12+
13+
public abstract class AvroJavaTimeDeserializerBase<T> extends StdScalarDeserializer<T> {
14+
15+
protected AvroJavaTimeDeserializerBase(Class<T> supportedType) {
16+
super(supportedType);
17+
}
18+
19+
@Override
20+
public LogicalType logicalType() {
21+
return LogicalType.DateTime;
22+
}
23+
24+
@SuppressWarnings("unchecked")
25+
@Override
26+
public T deserialize(JsonParser p, DeserializationContext context) throws IOException {
27+
if (p.getCurrentToken() == VALUE_NUMBER_INT) {
28+
final ZoneId defaultZoneId = context.getTimeZone().toZoneId().normalized();
29+
return fromLong(p.getLongValue(), defaultZoneId);
30+
} else {
31+
return (T) context.handleUnexpectedToken(_valueClass, p);
32+
}
33+
}
34+
35+
protected abstract T fromLong(long longValue, ZoneId defaultZoneId);
36+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;
2+
3+
import java.time.LocalDate;
4+
import java.time.ZoneId;
5+
6+
/**
7+
* Deserializer for {@link LocalDate} from and integer value.
8+
*
9+
* Deserialized value represents number of days from the unix epoch, 1 January 1970.
10+
*
11+
* Deserialization from string is not supported.
12+
*/
13+
public class AvroLocalDateDeserializer extends AvroJavaTimeDeserializerBase<LocalDate> {
14+
15+
private static final long serialVersionUID = 1L;
16+
17+
public static final AvroLocalDateDeserializer INSTANCE = new AvroLocalDateDeserializer();
18+
19+
protected AvroLocalDateDeserializer() {
20+
super(LocalDate.class);
21+
}
22+
23+
@Override
24+
protected LocalDate fromLong(long longValue, ZoneId defaultZoneId) {
25+
/**
26+
* Number of days from the unix epoch, 1 January 1970..
27+
*/
28+
return LocalDate.ofEpochDay(longValue);
29+
}
30+
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;
2+
3+
import java.time.Instant;
4+
import java.time.LocalDateTime;
5+
import java.time.ZoneId;
6+
import java.time.ZoneOffset;
7+
8+
/**
9+
* Deserializer for {@link LocalDateTime} from an integer value.
10+
*
11+
* Deserialized value represents timestamp in a local timezone, regardless of what specific time zone
12+
* is considered local, with a precision of one millisecond from 1 January 1970 00:00:00.000.
13+
*
14+
* Deserialization from string is not supported.
15+
*/
16+
public class AvroLocalDateTimeDeserializer extends AvroJavaTimeDeserializerBase<LocalDateTime> {
17+
18+
private static final long serialVersionUID = 1L;
19+
20+
public static final AvroLocalDateTimeDeserializer INSTANCE = new AvroLocalDateTimeDeserializer();
21+
22+
protected AvroLocalDateTimeDeserializer() {
23+
super(LocalDateTime.class);
24+
}
25+
26+
@Override
27+
protected LocalDateTime fromLong(long longValue, ZoneId defaultZoneId) {
28+
/**
29+
* Number of milliseconds in a local timezone, regardless of what specific time zone is considered local,
30+
* from 1 January 1970 00:00:00.000.
31+
*/
32+
return LocalDateTime.ofInstant(Instant.ofEpochMilli(longValue), ZoneOffset.ofTotalSeconds(0));
33+
}
34+
35+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.deser;
2+
3+
import java.time.LocalTime;
4+
import java.time.ZoneId;
5+
6+
/**
7+
* Deserializer for {@link LocalTime} from an integer value.
8+
*
9+
* Deserialized value represents time of day, with no reference to a particular calendar,
10+
* time zone or date, where the int stores the number of milliseconds after midnight, 00:00:00.000.
11+
*
12+
* Deserialization from string is not supported.
13+
*/
14+
public class AvroLocalTimeDeserializer extends AvroJavaTimeDeserializerBase<LocalTime> {
15+
16+
private static final long serialVersionUID = 1L;
17+
18+
public static final AvroLocalTimeDeserializer INSTANCE = new AvroLocalTimeDeserializer();
19+
20+
protected AvroLocalTimeDeserializer() {
21+
super(LocalTime.class);
22+
}
23+
24+
@Override
25+
protected LocalTime fromLong(long longValue, ZoneId defaultZoneId) {
26+
/**
27+
* Number of milliseconds, with no reference to a particular calendar, time zone or date, after
28+
* midnight, 00:00:00.000.
29+
*/
30+
return LocalTime.ofNanoOfDay(longValue * 1000_000L);
31+
}
32+
33+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.fasterxml.jackson.dataformat.avro.jsr310.ser;
2+
3+
import com.fasterxml.jackson.core.JsonGenerator;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.databind.JavaType;
6+
import com.fasterxml.jackson.databind.JsonMappingException;
7+
import com.fasterxml.jackson.databind.SerializerProvider;
8+
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
9+
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
10+
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
11+
12+
import java.io.IOException;
13+
import java.time.Instant;
14+
import java.time.OffsetDateTime;
15+
import java.time.ZonedDateTime;
16+
import java.time.temporal.Temporal;
17+
import java.util.function.Function;
18+
19+
/**
20+
* Serializer for variants of java.time classes (Instant, OffsetDateTime, ZonedDateTime) into long value.
21+
*
22+
* Serialized value represents an instant on the global timeline, independent of a particular time zone or
23+
* calendar, with a precision of one millisecond from the unix epoch, 1 January 1970 00:00:00.000 UTC.
24+
* Please note that time zone information gets lost in this process. Upon reading a value back, we can only
25+
* reconstruct the instant, but not the original representation.
26+
*
27+
* Note: In combination with {@link com.fasterxml.jackson.dataformat.avro.schema.DateTimeVisitor} it aims to produce
28+
* Avro schema with type long and logicalType timestamp-millis:
29+
* {
30+
* "type" : "long",
31+
* "logicalType" : "timestamp-millis"
32+
* }
33+
*
34+
* {@link AvroInstantSerializer} does not support serialization to string.
35+
*
36+
* @param <T> The type of a instant class that can be serialized.
37+
*/
38+
public class AvroInstantSerializer<T extends Temporal> extends StdScalarSerializer<T> {
39+
40+
private static final long serialVersionUID = 1L;
41+
42+
public static final AvroInstantSerializer<Instant> INSTANT =
43+
new AvroInstantSerializer<>(Instant.class, Function.identity());
44+
45+
public static final AvroInstantSerializer<OffsetDateTime> OFFSET_DATE_TIME =
46+
new AvroInstantSerializer<>(OffsetDateTime.class, OffsetDateTime::toInstant);
47+
48+
public static final AvroInstantSerializer<ZonedDateTime> ZONED_DATE_TIME =
49+
new AvroInstantSerializer<>(ZonedDateTime.class, ZonedDateTime::toInstant);
50+
51+
private final Function<T, Instant> getInstant;
52+
53+
protected AvroInstantSerializer(Class<T> t, Function<T, Instant> getInstant) {
54+
super(t);
55+
this.getInstant = getInstant;
56+
}
57+
58+
@Override
59+
public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException {
60+
/**
61+
* Number of milliseconds, independent of a particular time zone or calendar,
62+
* from 1 January 1970 00:00:00.000 UTC.
63+
*/
64+
final Instant instant = getInstant.apply(value);
65+
gen.writeNumber(instant.toEpochMilli());
66+
}
67+
68+
@Override
69+
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint) throws JsonMappingException {
70+
JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
71+
if (v2 != null) {
72+
v2.numberType(JsonParser.NumberType.LONG);
73+
}
74+
}
75+
76+
}

0 commit comments

Comments
 (0)