1. 概述

本文将深入探讨如何在 Java 中将 JSON 数据转换为 Apache Avro 对象。Avro 是一个数据序列化框架,提供丰富的数据结构和紧凑的二进制数据格式。 与其他序列化框架不同,Avro 使用 JSON 格式定义模式(schema),无需为序列化生成代码。

其核心优势在于支持模式演进(schema evolution),特别适合处理随时间变化的数据结构。同时,紧凑的数据格式使其成为高数据量处理场景的理想选择。

2. JSON 转 Avro 转换

在 Avro 中,JSON 转对象需要两个关键要素:定义数据结构的模式(schema)和转换机制(本文通过 convertJsonToAvro() 方法实现)。

模式定义数据格式(包括字段名和类型),而转换方法则利用该模式将 JSON 转换为 Avro 对象。

2.1. 实现转换方法

首先添加 Maven 依赖

<dependency>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro</artifactId>
    <version>1.12.0</version>
</dependency>

接着创建 JSON 需遵循的模式:

private static final String SCHEMA_JSON = """
    {
        "type": "record",
        "name": "Customer",
        "namespace": "com.baeldung.avro",
        "fields": [
            {"name": "name", "type": "string"},
            {"name": "age", "type": "int"},
            {"name": "email", "type": ["null", "string"], "default": null}
        ]
    }""";

然后实现转换方法。转换过程包含三个核心组件:模式、根据模式解析 JSON 的解码器(decoder)、创建 Avro 对象的 DatumReader

GenericRecord convertJsonToAvro(String json) throws IOException {
    
    try {
        DatumReader<GenericRecord> reader = new GenericDatumReader<>(schema);
        Decoder decoder = DecoderFactory.get().jsonDecoder(schema, json);
        return reader.read(null, decoder);
    } catch (IOException e) {
        throw new IOException("Error converting JSON to Avro", e);
    }
}

解码器验证 JSON 输入是否符合模式结构,DatumReader 则结合模式和解码器创建 GenericRecord 对象,无需生成特定类即可表示数据。

转换流程如下:

  1. ✅ 根据模式验证 JSON 输入
  2. ✅ 解码器按模式结构解析 JSON
  3. DatumReader 创建包含数据的 GenericRecord
  4. ✅ JSON 中缺失但模式中定义的字段自动赋予默认值

⚠️ 联合类型(union types)的特殊处理:当字段定义为联合类型(如 ["null", "string"]),JSON 必须显式指定类型。例如字符串值需包装为 {"string": "value"},而非直接使用 "value"

2.2. 测试 JSON 转 Avro 转换

测试完整 JSON 对象转换:

@Test
void whenValidJsonInput_thenConvertsToAvro() throws IOException {
    
    JsonToAvroConverter converter = new JsonToAvroConverter();
    String json = "{\"name\":\"John Doe\",\"age\":30,\"email\":{\"string\":\"john.doe@example.com\"}}";

    GenericRecord record = converter.convertJsonToAvro(json);

    assertEquals("John Doe", record.get("name").toString());
    assertEquals(30, record.get("age"));
    assertEquals("john.doe@example.com", record.get("email").toString());
}

注意 email 字段的特殊格式:这是 Avro 联合类型的语法要求。

测试含 null 字段的 JSON 转换:

@Test
void whenJsonWithNullableField_thenConvertsToAvro() throws IOException {
    
    JsonToAvroConverter converter = new JsonToAvroConverter();
    String json = "{\"name\":\"John Doe\",\"age\":30,\"email\":null}";

    GenericRecord record = converter.convertJsonToAvro(json);

    assertEquals("John Doe", record.get("name").toString());
    assertEquals(30, record.get("age"));
    assertNull(record.get("email"));
}

该测试验证了可空字段(email 定义为 ["null", "string"])正确处理 null 值。

3. 高级用法

基础转换覆盖多数场景,但实际应用常需更复杂操作。下面探讨两个关键场景:处理 JSON 数组和二进制序列化。

3.1. 处理 JSON 数组

批量处理多个 JSON 对象是常见需求。 扩展转换器以支持数组:

List<GenericRecord> convertJsonArrayToAvro(String jsonArray) throws IOException {
    
    List<GenericRecord> records = new ArrayList<>();
    
    Schema arraySchema = Schema.createArray(schema);
    
    Decoder decoder = DecoderFactory.get().jsonDecoder(arraySchema, jsonArray);
    DatumReader<List<GenericRecord>> reader = new GenericDatumReader<>(arraySchema);
    
    List<GenericRecord> result = reader.read(null, decoder);
    return result;
}

方法解析:

  1. 创建数组模式(基于记录模式)
  2. 使用 Avro 内置 JSON 解码器验证结构
  3. 自动处理联合类型等特殊字段
  4. 通过 DatumReader 一次性读取整个数组

3.2. 测试 JSON 数组处理

验证数组转换功能:

@Test
void whenJsonArray_thenConvertsToAvroList() throws IOException {
    
    JsonToAvroConverter converter = new JsonToAvroConverter();
    String jsonArray = """
        [
            {"name":"John Doe","age":30,"email":{"string":"john.doe@example.com"}},
            {"name":"Jane Doe","age":28,"email":{"string":"jane.doe@example.com"}}
        ]""";

    List<GenericRecord> records = converter.convertJsonArrayToAvro(jsonArray);

    assertEquals(2, records.size());
    assertEquals("John Doe", records.get(0).get("name").toString());
    assertEquals("jane.doe@example.com", records.get(1).get("email").toString());
}

⚠️ 注意:即使数组中,联合类型字段(如 email)仍需保持 Avro JSON 格式。

3.3. 二进制序列化

JSON 转 Avro 对象虽便于处理,但实际应用常需高效存储或传输数据。Avro 的二进制序列化相比 JSON/XML 具有显著优势:

  • 更紧凑的数据格式
  • 更高的序列化/反序列化性能
  • 内置模式演进支持

实现 GenericRecord 二进制序列化:

byte[] serializeAvroRecord(GenericRecord record) throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    DatumWriter<GenericRecord> writer = new GenericDatumWriter<>(schema);
    BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(outputStream, null);
    
    writer.write(record, encoder);
    encoder.flush();
    return outputStream.toByteArray();
}

3.4. 测试二进制序列化

验证序列化功能:

@Test
void whenSerializingAvroRecord_thenProducesByteArray() throws IOException {
    String json = """ 
        {"name":"John Doe","age":30,"email":{"string":"john.doe@example.com"}}
        """ ;
    JsonToAvroConverter converter = new JsonToAvroConverter();
    GenericRecord record = converter.convertJsonToAvro(json);

    byte[] bytes = converter.serializeAvroRecord(record);

    assertNotNull(bytes);
    assertTrue(bytes.length > 0);
}

测试流程:

  1. JSON → GenericRecord
  2. 通过 serializeAvroRecord() 二进制序列化
  3. 验证输出为非空字节数组

4. 总结

本文系统介绍了 Java 中 JSON 转 Avro 对象的完整流程,涵盖基础转换、数组处理、二进制序列化和验证等核心场景。通过实战示例展示了 Avro 的关键特性:

  • ✅ 模式驱动的数据转换
  • ✅ 联合类型与可空字段处理
  • ✅ 批量数据处理能力
  • ✅ 高效二进制序列化

这些技术为 Java 应用中处理 JSON 和 Avro 提供了坚实基础,特别适合需要高性能数据序列化和模式演进的系统。


原始标题:Convert JSON to Avro Object | Baeldung