diff --git a/doc/index.md b/doc/index.md
index df2865b..53edc3c 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -15,27 +15,37 @@ This document describes the various functionality in more detail.
Parsing
-------
-The main day-to-day use of this library is to parse records in various formats into Avro. As such,
-you won't find a converter for (for example) CSV files: these are container files with multiple
-records.
+The main day-to-day use of this library is to parse single records in various formats into Avro. As
+a result, you won't find a converter for (for example) CSV files: these are container files with
+multiple records.
The following formats can be converted to Avro:
-| Format | Parser constructor |
-|--------------------|----------------------------------------------------------------------------------------------|
-| JSON (with schema) | `opwvhk.avro.json.JsonAsAvroParser#JsonAsAvroParser(URI, boolean, Schema, GenericData)` |
-| JSON (unvalidated) | `opwvhk.avro.json.JsonAsAvroParser#JsonAsAvroParser(Schema, GenericData)` |
-| XML (with XSD) | `opwvhk.avro.xml.XmlAsAvroParser#XmlAsAvroParser(URL, String, boolean, Schema, GenericData)` |
-| XML (unvalidated) | `opwvhk.avro.xml.XmlAsAvroParser#XmlAsAvroParser(Schema, GenericData)` |
+| Format | Parser class |
+|--------|-------------------------------------|
+| JSON | `opwvhk.avro.json.JsonAsAvroParser` |
+| XML | `opwvhk.avro.xml.XmlAsAvroParser` |
-Parsers all use both a write schema and a read schema, just like Avro does. The write schema is used
-to validate the input, and the read schema is used to describe the result.
+Parsers require a read schema and an Avro model, determining the Avro record type to parse data into
+and how to create the these records, respectively. Additionally, they support a format dependent
+"write schema" (i.e., JSON schema, XSD, …), which is used for schema validation, and can be
+used for input validation.
+
+### Schema evolution
When parsing/converting data, the conversion can do implicit conversions that "fit". This includes
like widening conversions (like int→long), lossy conversions (like decimal→float or anything→string)
and parsing dates. With a write schema, binary conversions (from hexadecimal/base64 encoded text)
are also supported.
+In addition, the read schema is used for schema evolution:
+
+* removing fields: fields that are not present in the read schema will be ignored
+* adding fields: fields that are not present in the input will be filled with the default values
+ from the read schema
+* renaming fields: field aliases are also used to match incoming data, effectively renaming these
+ fields
+
### Source schema optional but encouraged
The parsers support as much functionality as possible when the write (source) schema is omitted.
@@ -43,6 +53,8 @@ However, this is discouraged. The reason is that significant functionality is mi
* No check on required fields:
The parsers will happily generate incomplete records, which **will** break when using them.
+* No check on compatibility:
+ Incompatible data cannot be detected, which **will** break the parsing process.
* No input validation:
Without a schema, a parser cannot validate input. This can cause unpredictable failures later on.
diff --git a/src/main/java/opwvhk/avro/io/AsAvroParserBase.java b/src/main/java/opwvhk/avro/io/AsAvroParserBase.java
index d411f91..0b7a8fc 100644
--- a/src/main/java/opwvhk/avro/io/AsAvroParserBase.java
+++ b/src/main/java/opwvhk/avro/io/AsAvroParserBase.java
@@ -390,8 +390,9 @@ protected ValueResolver createResolver(WriteSchema writeSchema, Schema readSchem
*
*
*
- * - There is no early detection of incompatible types; exceptions due to incompatibilities will happen (at best) while parsing, but can occur later
- * .
+ * -
+ * There is no early detection of incompatible types; exceptions due to incompatibilities will happen (at best) while parsing, but can occur later.
+ *
*
* -
* Specifically, it is impossible to determine if required fields may be missing. This will not cause problems when parsing, but will cause problems
diff --git a/src/main/java/opwvhk/avro/json/JsonAsAvroParser.java b/src/main/java/opwvhk/avro/json/JsonAsAvroParser.java
index 2b6205f..d9158e5 100644
--- a/src/main/java/opwvhk/avro/json/JsonAsAvroParser.java
+++ b/src/main/java/opwvhk/avro/json/JsonAsAvroParser.java
@@ -106,25 +106,26 @@ private static boolean isValidEnum(SchemaProperties writeType, Schema readSchema
* @param model the Avro model used to create records
*/
public JsonAsAvroParser(URI jsonSchemaLocation, Schema readSchema, GenericData model) {
- this(jsonSchemaLocation, true, readSchema, model);
+ this(jsonSchemaLocation, true, readSchema, Set.of(), model);
}
/**
* Create a JSON parser using only the specified Avro schema. The parse result will match the schema, but can be invalid if {@code validateInput} is set to
* {@code false}.
*
- * @param jsonSchemaLocation the location of the JSON (write) schema (schema of the JSON data to parse)
- * @param validateInput if {@code true}, validate the input when parsing
- * @param readSchema the read schema (schema of the resulting records)
- * @param model the Avro model used to create records
+ * @param jsonSchemaLocation the location of the JSON (write) schema (schema of the JSON data to parse)
+ * @param validateInput if {@code true}, validate the input when parsing
+ * @param readSchema the read schema (schema of the resulting records)
+ * @param fieldsAllowedMissing fields in the read schema that are allowed to be missing, even when this yields invalid records
+ * @param model the Avro model used to create records
*/
- public JsonAsAvroParser(URI jsonSchemaLocation, boolean validateInput, Schema readSchema, GenericData model) {
- this(model, analyseJsonSchema(jsonSchemaLocation), readSchema, validateInput);
+ public JsonAsAvroParser(URI jsonSchemaLocation, boolean validateInput, Schema readSchema, Set fieldsAllowedMissing, GenericData model) {
+ this(model, analyseJsonSchema(jsonSchemaLocation), readSchema, fieldsAllowedMissing, validateInput);
}
private static SchemaProperties analyseJsonSchema(URI jsonSchemaLocation) {
SchemaAnalyzer schemaAnalyzer = new SchemaAnalyzer();
- return schemaAnalyzer.parseJsonProperties(jsonSchemaLocation);
+ return schemaAnalyzer.parseJsonProperties(jsonSchemaLocation);
}
/**
@@ -135,11 +136,11 @@ private static SchemaProperties analyseJsonSchema(URI jsonSchemaLocation) {
* @param model the Avro model used to create records
*/
public JsonAsAvroParser(Schema readSchema, GenericData model) {
- this(model, null, readSchema, false);
+ this(model, null, readSchema, Set.of(), false);
}
- private JsonAsAvroParser(GenericData model, SchemaProperties schemaProperties, Schema readSchema, boolean validateInput) {
- super(model, schemaProperties, readSchema, Set.of());
+ private JsonAsAvroParser(GenericData model, SchemaProperties schemaProperties, Schema readSchema, Set fieldsAllowedMissing, boolean validateInput) {
+ super(model, schemaProperties, readSchema, fieldsAllowedMissing);
resolver = createResolver(schemaProperties, readSchema);
mapper = new ObjectMapper();
if (validateInput) {
diff --git a/src/test/java/opwvhk/avro/json/JsonAsAvroParserTest.java b/src/test/java/opwvhk/avro/json/JsonAsAvroParserTest.java
index ce88d9c..6b34346 100644
--- a/src/test/java/opwvhk/avro/json/JsonAsAvroParserTest.java
+++ b/src/test/java/opwvhk/avro/json/JsonAsAvroParserTest.java
@@ -1,89 +1,90 @@
package opwvhk.avro.json;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Objects;
-
import opwvhk.avro.ResolvingFailure;
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericRecord;
import org.junit.jupiter.api.Test;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Objects;
+import java.util.Set;
+
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class JsonAsAvroParserTest {
- @Test
- void testMostTypesWithJsonSchema() throws IOException, URISyntaxException {
- Schema readSchema = avroSchema("TestRecord.avsc");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(resourceUri("TestRecord.schema.json"), readSchema, GenericData.get());
- GenericRecord fullRecord = parser.parse(getClass().getResource("TestRecord-full.json"));
-
- assertThat(fullRecord.toString()).isEqualTo(
- ("{'bool': true, 'shortInt': 42, 'longInt': 6789012345, 'hugeInt': 123456789012345678901, 'defaultInt': 4242, " +
- "'singleFloat': 123.456, 'doubleFloat': 1234.56789, 'fixedPoint': 12345678901.123456, 'defaultNumber': 98765.4321, " +
- "'choice': 'maybe', 'date': '2023-04-17', 'time': '17:08:34.567123Z', 'timestamp': '2023-04-17T17:08:34.567123Z', " +
- "'binary': 'Hello World!', 'hexBytes': 'Hello World!', 'texts': ['Hello', 'World!'], 'weirdStuff': {" +
- "'explanation': 'No reason. I just felt like it.', 'fancy': '\uD83D\uDE04! You are on Candid Camera! \uD83D\uDCF9\uD83C\uDF4C', " +
- "'rabbitHole': null}}").replace('\'', '"'));
- }
-
- @Test
- void testParsingDatesAndTimesWithJsonSchema() throws IOException, URISyntaxException {
- Schema readSchema = avroSchema("DatesAndTimes.avsc");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(resourceUri("DatesAndTimes.schema.json"), readSchema, GenericData.get());
- GenericRecord minimalRecord = parser.parse("""
- {"dateOnly": "2023-04-17", "timeMillis": "17:08:34.567+02:00", "timeMicros": "17:08:34.567123Z",
- "timestampMillis": "2023-04-17T17:08:34.567+02:00", "timestampMicros": "2023-04-17T17:08:34.567123+02:00",
- "localTimestampMillis": "2023-04-17T17:08:34.567", "localTimestampMicros": "2023-04-17T17:08:34.567123"
- }""");
-
- assertThat(minimalRecord.toString()).isEqualTo(
- "{\"dateOnly\": \"2023-04-17\", \"timeMillis\": \"17:08:34.567+02:00\", \"timeMicros\": \"17:08:34.567123Z\", " +
- "\"timestampMillis\": \"2023-04-17T15:08:34.567Z\", \"timestampMicros\": \"2023-04-17T15:08:34.567123Z\", " +
- "\"localTimestampMillis\": \"2023-04-17T17:08:34.567\", \"localTimestampMicros\": \"2023-04-17T17:08:34.567123\"}");
- }
-
- @Test
- void testParsingEnumDifferentlyThanJsonSchema() throws IOException, URISyntaxException {
- URI jsonSchema = resourceUri("TestRecord.schema.json");
- JsonAsAvroParser parser;
- GenericRecord record;
-
- parser = new JsonAsAvroParser(jsonSchema, false, avroSchema("DifferentChoiceWithDefault.avsc"), GenericData.get());
- record = parser.parse("""
- {"choice": "no"}""");
-
- assertThat(record.toString()).isEqualTo(
- "{\"choice\": \"vielleicht\"}");
-
- parser = new JsonAsAvroParser(jsonSchema, false, avroSchema("ChoiceAsString.avsc"), GenericData.get());
- record = parser.parse("""
- {"choice": "no"}""");
-
- assertThat(record.toString()).isEqualTo(
- "{\"choice\": \"no\"}");
- }
-
- @Test
- void testResolveFailuresWithJsonSchema() throws URISyntaxException {
- URI jsonSchema = resourceUri("TestRecord.schema.json");
- GenericData model = GenericData.get();
-
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("RequiredShortInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("NotAnInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooShortDecimal.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooImpreciseDecimal.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("DifferentChoice.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("ChoiceAsInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooShortInteger.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("NonNullableInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
- }
+ @Test
+ void testMostTypesWithJsonSchema() throws IOException, URISyntaxException {
+ Schema readSchema = avroSchema("TestRecord.avsc");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(resourceUri("TestRecord.schema.json"), readSchema, GenericData.get());
+ GenericRecord fullRecord = parser.parse(getClass().getResource("TestRecord-full.json"));
+
+ assertThat(fullRecord.toString()).isEqualTo(
+ ("{'bool': true, 'shortInt': 42, 'longInt': 6789012345, 'hugeInt': 123456789012345678901, 'defaultInt': 4242, " +
+ "'singleFloat': 123.456, 'doubleFloat': 1234.56789, 'fixedPoint': 12345678901.123456, 'defaultNumber': 98765.4321, " +
+ "'choice': 'maybe', 'date': '2023-04-17', 'time': '17:08:34.567123Z', 'timestamp': '2023-04-17T17:08:34.567123Z', " +
+ "'binary': 'Hello World!', 'hexBytes': 'Hello World!', 'texts': ['Hello', 'World!'], 'weirdStuff': {" +
+ "'explanation': 'No reason. I just felt like it.', 'fancy': '\uD83D\uDE04! You are on Candid Camera! \uD83D\uDCF9\uD83C\uDF4C', " +
+ "'rabbitHole': null}}").replace('\'', '"'));
+ }
+
+ @Test
+ void testParsingDatesAndTimesWithJsonSchema() throws IOException, URISyntaxException {
+ Schema readSchema = avroSchema("DatesAndTimes.avsc");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(resourceUri("DatesAndTimes.schema.json"), readSchema, GenericData.get());
+ GenericRecord minimalRecord = parser.parse("""
+ {"dateOnly": "2023-04-17", "timeMillis": "17:08:34.567+02:00", "timeMicros": "17:08:34.567123Z",
+ "timestampMillis": "2023-04-17T17:08:34.567+02:00", "timestampMicros": "2023-04-17T17:08:34.567123+02:00",
+ "localTimestampMillis": "2023-04-17T17:08:34.567", "localTimestampMicros": "2023-04-17T17:08:34.567123"
+ }""");
+
+ assertThat(minimalRecord.toString()).isEqualTo(
+ "{\"dateOnly\": \"2023-04-17\", \"timeMillis\": \"17:08:34.567+02:00\", \"timeMicros\": \"17:08:34.567123Z\", " +
+ "\"timestampMillis\": \"2023-04-17T15:08:34.567Z\", \"timestampMicros\": \"2023-04-17T15:08:34.567123Z\", " +
+ "\"localTimestampMillis\": \"2023-04-17T17:08:34.567\", \"localTimestampMicros\": \"2023-04-17T17:08:34.567123\"}");
+ }
+
+ @Test
+ void testParsingEnumDifferentlyThanJsonSchema() throws IOException, URISyntaxException {
+ URI jsonSchema = resourceUri("TestRecord.schema.json");
+ JsonAsAvroParser parser;
+ GenericRecord record;
+
+ parser = new JsonAsAvroParser(jsonSchema, false, avroSchema("DifferentChoiceWithDefault.avsc"), Set.of(), GenericData.get());
+ record = parser.parse("""
+ {"choice": "no"}""");
+
+ assertThat(record.toString()).isEqualTo(
+ "{\"choice\": \"vielleicht\"}");
+
+ parser = new JsonAsAvroParser(jsonSchema, false, avroSchema("ChoiceAsString.avsc"), Set.of(), GenericData.get());
+ record = parser.parse("""
+ {"choice": "no"}""");
+
+ assertThat(record.toString()).isEqualTo(
+ "{\"choice\": \"no\"}");
+ }
+
+ @Test
+ void testResolveFailuresWithJsonSchema() throws URISyntaxException {
+ URI jsonSchema = resourceUri("TestRecord.schema.json");
+ GenericData model = GenericData.get();
+
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("RequiredShortInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("NotAnInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooShortDecimal.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooImpreciseDecimal.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("DifferentChoice.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("ChoiceAsInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("TooShortInteger.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ assertThatThrownBy(() -> new JsonAsAvroParser(jsonSchema, avroSchema("NonNullableInt.avsc"), model)).isInstanceOf(ResolvingFailure.class);
+ }
@Test
void testValidationFailure() throws IOException, URISyntaxException {
@@ -93,79 +94,79 @@ void testValidationFailure() throws IOException, URISyntaxException {
assertThatThrownBy(() -> parser.parse("{}")).isInstanceOf(IOException.class).hasMessage("Invalid JSON");
}
- @Test
- void testMostTypesFromAvroSchema() throws IOException {
- Schema readSchema = avroSchema("TestRecordProjection.avsc");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
- GenericRecord fullRecord = parser.parse(getClass().getResource("TestRecord-full.json"));
-
- assertThat(fullRecord.toString()).isEqualTo(
- ("{'bool': true, 'shortInt': 42, 'longInt': 6789012345, 'hugeInt': 123456789012345678901, 'defaultInt': 4242, " +
- "'singleFloat': 123.456, 'doubleFloat': 1234.56789, 'fixedPoint': 12345678901.123456, 'defaultNumber': 98765.4321, " +
- "'choice': 'maybe', 'date': '2023-04-17', 'time': '17:08:34.567123Z', 'timestamp': '2023-04-17T17:08:34.567123Z', " +
- "'texts': ['Hello', 'World!'], 'weirdStuff': {'explanation': 'No reason. I just felt like it.', " +
- "'fancy': '\uD83D\uDE04! You are on Candid Camera! \uD83D\uDCF9\uD83C\uDF4C', 'rabbitHole': null}}").replace('\'', '"'));
- }
-
- @Test
- void testMinimalRecordFromAvroSchema() throws IOException {
- Schema readSchema = avroSchema("TestRecordProjection.avsc");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
- GenericRecord minimalRecord = parser.parse("""
- {
- "bool": false,
- "shortInt": null,
- "choice": "no",
- "texts": [],
- "weirdStuff": { }
- }""");
-
- assertThat(minimalRecord.toString()).isEqualTo(
- ("{'bool': false, 'shortInt': null, 'longInt': null, 'hugeInt': null, 'defaultInt': 42, " +
- "'singleFloat': null, 'doubleFloat': null, 'fixedPoint': null, 'defaultNumber': 4.2, 'choice': 'no', " +
- "'date': null, 'time': null, 'timestamp': null, " +
- "'texts': [], 'weirdStuff': {'explanation': 'Please explain why', " +
- "'fancy': null, 'rabbitHole': null}}").replace('\'', '"'));
- }
-
- @Test
- void testParsingDatesAndTimesFromAvro() throws IOException {
- Schema readSchema = avroSchema("DatesAndTimes.avsc");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
- GenericRecord minimalRecord = parser.parse("""
- {"dateOnly": "2023-04-17", "timeMillis": "17:08:34.567+02:00", "timeMicros": "17:08:34.567123",
- "timestampMillis": "2023-04-17T17:08:34.567123CET", "timestampMicros": "2023-04-17T17:08:34.567123+02:00",
- "localTimestampMillis": "2023-04-17T17:08:34.567", "localTimestampMicros": "2023-04-17T17:08:34.567123"
- }""");
-
- assertThat(minimalRecord.toString()).isEqualTo(
- "{\"dateOnly\": \"2023-04-17\", \"timeMillis\": \"17:08:34.567+02:00\", \"timeMicros\": \"17:08:34.567123Z\", " +
- "\"timestampMillis\": \"2023-04-17T15:08:34.567123Z\", \"timestampMicros\": \"2023-04-17T15:08:34.567123Z\", " +
- "\"localTimestampMillis\": \"2023-04-17T17:08:34.567\", \"localTimestampMicros\": \"2023-04-17T17:08:34.567123\"}");
- }
-
- @Test
- void testParsingObjectsAndArraysForScalarsFails() {
- Schema readSchema = new Schema.Parser().parse("""
- {"type": "record", "name": "Oops", "fields": [
- {"name": "text", "type": "string"}
- ]}""");
-
- JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
- assertThatThrownBy(() -> parser.parse("{\"text\": {}}")).isInstanceOf(IllegalStateException.class);
- assertThatThrownBy(() -> parser.parse("{\"text\": []}")).isInstanceOf(IllegalStateException.class);
- }
-
- private Schema avroSchema(String avroSchemaResource) throws IOException {
- try (InputStream expectedSchemaStream = getClass().getResourceAsStream(avroSchemaResource)) {
- return new Schema.Parser().parse(expectedSchemaStream);
- }
- }
-
- private URI resourceUri(String resource) throws URISyntaxException {
- return Objects.requireNonNull(getClass().getResource(resource)).toURI();
- }
+ @Test
+ void testMostTypesFromAvroSchema() throws IOException {
+ Schema readSchema = avroSchema("TestRecordProjection.avsc");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
+ GenericRecord fullRecord = parser.parse(getClass().getResource("TestRecord-full.json"));
+
+ assertThat(fullRecord.toString()).isEqualTo(
+ ("{'bool': true, 'shortInt': 42, 'longInt': 6789012345, 'hugeInt': 123456789012345678901, 'defaultInt': 4242, " +
+ "'singleFloat': 123.456, 'doubleFloat': 1234.56789, 'fixedPoint': 12345678901.123456, 'defaultNumber': 98765.4321, " +
+ "'choice': 'maybe', 'date': '2023-04-17', 'time': '17:08:34.567123Z', 'timestamp': '2023-04-17T17:08:34.567123Z', " +
+ "'texts': ['Hello', 'World!'], 'weirdStuff': {'explanation': 'No reason. I just felt like it.', " +
+ "'fancy': '\uD83D\uDE04! You are on Candid Camera! \uD83D\uDCF9\uD83C\uDF4C', 'rabbitHole': null}}").replace('\'', '"'));
+ }
+
+ @Test
+ void testMinimalRecordFromAvroSchema() throws IOException {
+ Schema readSchema = avroSchema("TestRecordProjection.avsc");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
+ GenericRecord minimalRecord = parser.parse("""
+ {
+ "bool": false,
+ "shortInt": null,
+ "choice": "no",
+ "texts": [],
+ "weirdStuff": { }
+ }""");
+
+ assertThat(minimalRecord.toString()).isEqualTo(
+ ("{'bool': false, 'shortInt': null, 'longInt': null, 'hugeInt': null, 'defaultInt': 42, " +
+ "'singleFloat': null, 'doubleFloat': null, 'fixedPoint': null, 'defaultNumber': 4.2, 'choice': 'no', " +
+ "'date': null, 'time': null, 'timestamp': null, " +
+ "'texts': [], 'weirdStuff': {'explanation': 'Please explain why', " +
+ "'fancy': null, 'rabbitHole': null}}").replace('\'', '"'));
+ }
+
+ @Test
+ void testParsingDatesAndTimesFromAvro() throws IOException {
+ Schema readSchema = avroSchema("DatesAndTimes.avsc");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
+ GenericRecord minimalRecord = parser.parse("""
+ {"dateOnly": "2023-04-17", "timeMillis": "17:08:34.567+02:00", "timeMicros": "17:08:34.567123",
+ "timestampMillis": "2023-04-17T17:08:34.567123CET", "timestampMicros": "2023-04-17T17:08:34.567123+02:00",
+ "localTimestampMillis": "2023-04-17T17:08:34.567", "localTimestampMicros": "2023-04-17T17:08:34.567123"
+ }""");
+
+ assertThat(minimalRecord.toString()).isEqualTo(
+ "{\"dateOnly\": \"2023-04-17\", \"timeMillis\": \"17:08:34.567+02:00\", \"timeMicros\": \"17:08:34.567123Z\", " +
+ "\"timestampMillis\": \"2023-04-17T15:08:34.567123Z\", \"timestampMicros\": \"2023-04-17T15:08:34.567123Z\", " +
+ "\"localTimestampMillis\": \"2023-04-17T17:08:34.567\", \"localTimestampMicros\": \"2023-04-17T17:08:34.567123\"}");
+ }
+
+ @Test
+ void testParsingObjectsAndArraysForScalarsFails() {
+ Schema readSchema = new Schema.Parser().parse("""
+ {"type": "record", "name": "Oops", "fields": [
+ {"name": "text", "type": "string"}
+ ]}""");
+
+ JsonAsAvroParser parser = new JsonAsAvroParser(readSchema, GenericData.get());
+ assertThatThrownBy(() -> parser.parse("{\"text\": {}}")).isInstanceOf(IllegalStateException.class);
+ assertThatThrownBy(() -> parser.parse("{\"text\": []}")).isInstanceOf(IllegalStateException.class);
+ }
+
+ private Schema avroSchema(String avroSchemaResource) throws IOException {
+ try (InputStream expectedSchemaStream = getClass().getResourceAsStream(avroSchemaResource)) {
+ return new Schema.Parser().parse(expectedSchemaStream);
+ }
+ }
+
+ private URI resourceUri(String resource) throws URISyntaxException {
+ return Objects.requireNonNull(getClass().getResource(resource)).toURI();
+ }
}