Skip to content

Commit 28fa3cf

Browse files
authored
Fix #494 write enums as string (#496)
1 parent 55901a1 commit 28fa3cf

File tree

8 files changed

+250
-30
lines changed

8 files changed

+250
-30
lines changed

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/AvroSchemaGenerator.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@ public AvroSchemaGenerator disableLogicalTypes() {
3131
super.disableLogicalTypes();
3232
return this;
3333
}
34+
35+
@Override
36+
public AvroSchemaGenerator enableWriteEnumAsString() {
37+
super.enableWriteEnumAsString();
38+
return this;
39+
}
40+
41+
@Override
42+
public AvroSchemaGenerator disableWriteEnumAsString() {
43+
super.disableWriteEnumAsString();
44+
return this;
45+
}
46+
3447
}
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.schema;
2+
3+
import com.fasterxml.jackson.databind.BeanDescription;
4+
import com.fasterxml.jackson.databind.JavaType;
5+
import com.fasterxml.jackson.databind.SerializerProvider;
6+
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
7+
8+
import org.apache.avro.Schema;
9+
10+
import java.util.ArrayList;
11+
import java.util.Set;
12+
13+
/**
14+
* Specific visitor for Java Enum types that are to be exposed as
15+
* Avro Enums. Used unless Java Enums are to be mapped to Avro Strings.
16+
*
17+
* @since 2.18
18+
*/
19+
public class EnumVisitor extends JsonStringFormatVisitor.Base
20+
implements SchemaBuilder
21+
{
22+
protected final SerializerProvider _provider;
23+
protected final JavaType _type;
24+
protected final DefinedSchemas _schemas;
25+
26+
protected Set<String> _enums;
27+
28+
public EnumVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) {
29+
_schemas = schemas;
30+
_type = t;
31+
_provider = provider;
32+
}
33+
34+
@Override
35+
public void enumTypes(Set<String> enums) {
36+
_enums = enums;
37+
}
38+
39+
@Override
40+
public Schema builtAvroSchema() {
41+
if (_enums == null) {
42+
throw new IllegalStateException("Possible enum values cannot be null");
43+
}
44+
45+
BeanDescription bean = _provider.getConfig().introspectClassAnnotations(_type);
46+
Schema schema = AvroSchemaHelper.createEnumSchema(bean, new ArrayList<>(_enums));
47+
_schemas.addSchema(_type, schema);
48+
return schema;
49+
}
50+
}

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/StringVisitor.java

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3-
import java.util.ArrayList;
43
import java.util.Set;
54

65
import org.apache.avro.Schema;
@@ -18,13 +17,9 @@ public class StringVisitor extends JsonStringFormatVisitor.Base
1817
{
1918
protected final SerializerProvider _provider;
2019
protected final JavaType _type;
21-
protected final DefinedSchemas _schemas;
2220

23-
protected Set<String> _enums;
24-
25-
public StringVisitor(SerializerProvider provider, DefinedSchemas schemas, JavaType t) {
26-
_schemas = schemas;
27-
_type = t;
21+
public StringVisitor(SerializerProvider provider, JavaType type) {
22+
_type = type;
2823
_provider = provider;
2924
}
3025

@@ -35,7 +30,7 @@ public void format(JsonValueFormat format) {
3530

3631
@Override
3732
public void enumTypes(Set<String> enums) {
38-
_enums = enums;
33+
// Do nothing
3934
}
4035

4136
@Override
@@ -50,11 +45,6 @@ public Schema builtAvroSchema() {
5045
return AvroSchemaHelper.createUUIDSchema();
5146
}
5247
BeanDescription bean = _provider.getConfig().introspectClassAnnotations(_type);
53-
if (_enums != null) {
54-
Schema s = AvroSchemaHelper.createEnumSchema(bean, new ArrayList<>(_enums));
55-
_schemas.addSchema(_type, s);
56-
return s;
57-
}
5848
Schema schema = Schema.create(Schema.Type.STRING);
5949
// Stringable classes need to include the type
6050
if (AvroSchemaHelper.isStringable(bean.getClassInfo()) && !_type.hasRawClass(String.class)) {

avro/src/main/java/com/fasterxml/jackson/dataformat/avro/schema/VisitorFormatWrapperImpl.java

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
package com.fasterxml.jackson.dataformat.avro.schema;
22

3+
import java.time.temporal.Temporal;
4+
35
import com.fasterxml.jackson.core.JsonGenerator;
6+
47
import com.fasterxml.jackson.databind.JavaType;
58
import com.fasterxml.jackson.databind.JsonMappingException;
69
import com.fasterxml.jackson.databind.SerializerProvider;
710
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
8-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonAnyFormatVisitor;
9-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonArrayFormatVisitor;
10-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonBooleanFormatVisitor;
11-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
12-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonIntegerFormatVisitor;
13-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor;
14-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNullFormatVisitor;
15-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonNumberFormatVisitor;
16-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
17-
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
11+
import com.fasterxml.jackson.databind.jsonFormatVisitors.*;
12+
1813
import com.fasterxml.jackson.dataformat.avro.AvroSchema;
19-
import org.apache.avro.Schema;
2014

21-
import java.time.temporal.Temporal;
15+
import org.apache.avro.Schema;
2216

2317
public class VisitorFormatWrapperImpl
2418
implements JsonFormatVisitorWrapper
@@ -32,6 +26,11 @@ public class VisitorFormatWrapperImpl
3226
*/
3327
protected boolean _logicalTypesEnabled = false;
3428

29+
/**
30+
* @since 2.18
31+
*/
32+
protected boolean _writeEnumAsString = false;
33+
3534
/**
3635
* Visitor used for resolving actual Schema, if structured type
3736
* (or one with complex configuration)
@@ -105,6 +104,8 @@ public Schema getAvroSchema() {
105104

106105
/**
107106
* Enables Avro schema with Logical Types generation.
107+
*
108+
* @since 2.13
108109
*/
109110
public VisitorFormatWrapperImpl enableLogicalTypes() {
110111
_logicalTypesEnabled = true;
@@ -113,6 +114,8 @@ public VisitorFormatWrapperImpl enableLogicalTypes() {
113114

114115
/**
115116
* Disables Avro schema with Logical Types generation.
117+
*
118+
* @since 2.13
116119
*/
117120
public VisitorFormatWrapperImpl disableLogicalTypes() {
118121
_logicalTypesEnabled = false;
@@ -123,6 +126,31 @@ public boolean isLogicalTypesEnabled() {
123126
return _logicalTypesEnabled;
124127
}
125128

129+
/**
130+
* Enable Java enum to Avro string mapping.
131+
*
132+
* @since 2.18
133+
*/
134+
public VisitorFormatWrapperImpl enableWriteEnumAsString() {
135+
_writeEnumAsString = true;
136+
return this;
137+
}
138+
139+
/**
140+
* Disable Java enum to Avro string mapping.
141+
*
142+
* @since 2.18
143+
*/
144+
public VisitorFormatWrapperImpl disableWriteEnumAsString() {
145+
_writeEnumAsString = false;
146+
return this;
147+
}
148+
149+
// @since 2.18
150+
public boolean isWriteEnumAsStringEnabled() {
151+
return _writeEnumAsString;
152+
}
153+
126154
/*
127155
/**********************************************************************
128156
/* Callbacks
@@ -177,7 +205,16 @@ public JsonStringFormatVisitor expectStringFormat(JavaType type)
177205
_valueSchema = s;
178206
return null;
179207
}
180-
StringVisitor v = new StringVisitor(_provider, _schemas, type);
208+
209+
// 06-Jun-2024: [dataformats-binary#494] Enums may be exposed either
210+
// as native Avro Enums, or as Avro Strings:
211+
if (type.isEnumType() && !isWriteEnumAsStringEnabled()) {
212+
EnumVisitor v = new EnumVisitor(_provider, _schemas, type);
213+
_builder = v;
214+
return v;
215+
}
216+
217+
StringVisitor v = new StringVisitor(_provider, type);
181218
_builder = v;
182219
return v;
183220
}

avro/src/test/java/com/fasterxml/jackson/dataformat/avro/EnumTest.java

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
public class EnumTest extends AvroTestBase
44
{
5-
protected final static String ENUM_SCHEMA_JSON = "{\n"
5+
// gender as Avro enum
6+
protected final static String ENUM_SCHEMA_JSON = "{\n"
67
+"\"type\": \"record\",\n"
78
+"\"name\": \"Employee\",\n"
89
+"\"fields\": [\n"
@@ -11,6 +12,14 @@ public class EnumTest extends AvroTestBase
1112
+"}\n"
1213
+"]}";
1314

15+
// gender as Avro string
16+
protected final static String STRING_SCHEMA_JSON = "{"
17+
+" \"type\": \"record\", "
18+
+" \"name\": \"Employee\", "
19+
+" \"fields\": ["
20+
+" {\"name\": \"gender\", \"type\": \"string\"}"
21+
+"]}";
22+
1423
protected enum Gender { M, F; }
1524

1625
protected static class Employee {
@@ -23,14 +32,16 @@ protected static class EmployeeStr {
2332

2433
private final AvroMapper MAPPER = newMapper();
2534

26-
public void testSimple() throws Exception
35+
public void test_avroSchemaWithEnum_fromEnumValueToEnumValue() throws Exception
2736
{
2837
AvroSchema schema = MAPPER.schemaFrom(ENUM_SCHEMA_JSON);
2938
Employee input = new Employee();
3039
input.gender = Gender.F;
3140

3241
byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input);
3342
assertNotNull(bytes);
43+
// Enum Gender.M is encoded as bytes array: {0}, where DEC 0 is encoded long value 0, Gender.M ordinal value
44+
// Enum Gender.F is encoded as bytes array: {2}, where DEC 2 is encoded long value 1, Gender.F ordinal value
3445
assertEquals(1, bytes.length); // measured to be current exp size
3546

3647
// and then back
@@ -40,7 +51,7 @@ public void testSimple() throws Exception
4051
assertEquals(Gender.F, output.gender);
4152
}
4253

43-
public void testEnumValueAsString() throws Exception
54+
public void test_avroSchemaWithEnum_fromStringValueToEnumValue() throws Exception
4455
{
4556
AvroSchema schema = MAPPER.schemaFrom(ENUM_SCHEMA_JSON);
4657
EmployeeStr input = new EmployeeStr();
@@ -56,4 +67,55 @@ public void testEnumValueAsString() throws Exception
5667
assertNotNull(output);
5768
assertEquals(Gender.F, output.gender);
5869
}
70+
71+
public void test_avroSchemaWithString_fromEnumValueToEnumValue() throws Exception
72+
{
73+
AvroSchema schema = MAPPER.schemaFrom(STRING_SCHEMA_JSON);
74+
Employee input = new Employee();
75+
input.gender = Gender.F;
76+
77+
byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input);
78+
assertNotNull(bytes);
79+
// Enum Gender.F as string is encoded as {2, 70} bytes array.
80+
// Where
81+
// - DEC 2, HEX 0x2, is a long value 1 written using variable-length zig-zag coding.
82+
// It represents number of following characters in string "F"
83+
// - DEC 70, HEX 0x46, is UTF-8 code for letter F
84+
//
85+
// Enum Gender.M as string is encoded as {2, 77} bytes array.
86+
// Where
87+
// - DEC 2, HEX 0x2, is a long value 1. It is number of following characters in string "M"),
88+
// written using variable-length zig-zag coding.
89+
// - DEC 77, HEX 0x4D, is UTF-8 code for letter M
90+
//
91+
// See https://avro.apache.org/docs/1.8.2/spec.html#Encodings
92+
assertEquals(2, bytes.length); // measured to be current exp size
93+
assertEquals(0x2, bytes[0]);
94+
assertEquals(0x46, bytes[1]);
95+
96+
// and then back
97+
Employee output = MAPPER.readerFor(Employee.class).with(schema)
98+
.readValue(bytes);
99+
assertNotNull(output);
100+
assertEquals(Gender.F, output.gender);
101+
}
102+
103+
// Not sure this test makes sense
104+
public void test_avroSchemaWithString_fromStringValueToEnumValue() throws Exception
105+
{
106+
AvroSchema schema = MAPPER.schemaFrom(STRING_SCHEMA_JSON);
107+
EmployeeStr input = new EmployeeStr();
108+
input.gender = "F";
109+
110+
byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input);
111+
assertNotNull(bytes);
112+
assertEquals(2, bytes.length); // measured to be current exp size
113+
114+
// and then back
115+
Employee output = MAPPER.readerFor(Employee.class).with(schema)
116+
.readValue(bytes);
117+
assertNotNull(output);
118+
assertEquals(Gender.F, output.gender);
119+
}
120+
59121
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.fasterxml.jackson.dataformat.avro.schema;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.fasterxml.jackson.databind.JsonMappingException;
6+
import com.fasterxml.jackson.dataformat.avro.AvroMapper;
7+
import com.fasterxml.jackson.dataformat.avro.AvroTestBase;
8+
9+
import org.apache.avro.Schema;
10+
import org.apache.avro.specific.SpecificData;
11+
import org.junit.Test;
12+
13+
public class Enum_schemaCreationTest extends AvroTestBase {
14+
15+
static enum NumbersEnum {
16+
ONE, TWO, THREE
17+
}
18+
19+
private final AvroMapper MAPPER = newMapper();
20+
21+
@Test
22+
public void testJavaEnumToAvroEnum_test() throws JsonMappingException {
23+
// GIVEN
24+
AvroSchemaGenerator gen = new AvroSchemaGenerator();
25+
26+
// WHEN
27+
MAPPER.acceptJsonFormatVisitor(NumbersEnum.class , gen);
28+
Schema actualSchema = gen.getGeneratedSchema().getAvroSchema();
29+
30+
System.out.println("schema:\n" + actualSchema.toString(true));
31+
32+
// THEN
33+
assertThat(actualSchema.getType()).isEqualTo( Schema.Type.ENUM);
34+
assertThat(actualSchema.getEnumSymbols()).containsExactlyInAnyOrder("ONE", "TWO", "THREE");
35+
}
36+
37+
@Test
38+
public void testJavaEnumToAvroString_test() throws JsonMappingException {
39+
// GIVEN
40+
AvroSchemaGenerator gen = new AvroSchemaGenerator()
41+
.enableWriteEnumAsString();
42+
43+
// WHEN
44+
MAPPER.acceptJsonFormatVisitor(NumbersEnum.class , gen);
45+
Schema actualSchema = gen.getGeneratedSchema().getAvroSchema();
46+
47+
System.out.println("schema:\n" + actualSchema.toString(true));
48+
49+
// THEN
50+
assertThat(actualSchema.getType()).isEqualTo( Schema.Type.STRING);
51+
52+
// When type is stringable then java-class property is addded.
53+
assertThat(actualSchema.getProp(SpecificData.CLASS_PROP)).isNotEmpty();
54+
}
55+
56+
}

0 commit comments

Comments
 (0)