如何将Avro GenericRecord转换为不含Schema名称的JSON?
问题:Avro GenericRecord转JSON时嵌套类型出现全限定类名冗余
我手里有两个Avro Schema文件:
- Event.avsc:
{ "type": "record", "namespace": "com.onemount.jobs.transform.schema.avro", "name": "Event", "fields": [ { "name": "id", "type": "string" }, { "name": "mtp_interest_submit", "type": ["null", "InterestSubmitParam"], "default": null } ] }
- InterestSubmitParam.avsc:
{ "type": "record", "namespace": "com.onemount.jobs.transform.schema.avro", "name": "InterestSubmitParam", "fields": [ { "name": "interest", "type": { "type": "array", "items": "string" } } ] }
我正在从Kafka Confluent消费Avro消息(已经设置了specific.avro.reader=false),需要把GenericRecord转换成ObjectNode。目前用代码转换后得到的结果是这样的:
{ "id": "c8b76e58-9803-4c78-9f82-a185bda1cabf", "mtp_interest_submit": { "com.onemount.jobs.transform.schema.avro.InterestSubmitParam": { "interest": [ "fashion", "travel" ] } } }
但我想要的结果应该是去掉那个冗余的全限定类名,变成:
{ "id": "c8b76e58-9803-4c78-9f82-a185bda1cabf", "mtp_interest_submit": { "interest": [ "fashion", "travel" ] } }
我的转换代码是这样的:
GenericRecord genericRecord = ... try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { DatumWriter<GenericRecord> writer = new GenericDatumWriter<>(genericRecord.getSchema()); JsonEncoder encoder = EncoderFactory.get().jsonEncoder(genericRecord.getSchema(), outputStream); writer.write(genericRecord, encoder); encoder.flush(); return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); }
解决方法
这个问题的根源是:Avro的默认JsonEncoder在处理联合类型(比如你的mtp_interest_submit是null和自定义记录的联合)时,会自动把自定义类型的全限定类名作为key包裹嵌套对象,用来明确联合类型的具体实现。要去掉这个冗余,有两种简单的处理方式:
方法一:修改JsonEncoder配置,关闭类型信息输出
Avro的EncoderFactory提供了配置选项,让我们可以关闭联合类型的类型信息输出。在Avro 1.8及以上版本,只需要给jsonEncoder方法多传一个false参数(代表encodeType设为false)就行,代码改动极小:
GenericRecord genericRecord = ... try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { DatumWriter<GenericRecord> writer = new GenericDatumWriter<>(genericRecord.getSchema()); // 第三个参数设为false,关闭联合类型的类型信息输出 JsonEncoder encoder = EncoderFactory.get() .jsonEncoder(genericRecord.getSchema(), outputStream, false); writer.write(genericRecord, encoder); encoder.flush(); return new String(outputStream.toByteArray(), StandardCharsets.UTF_8); }
这种方式最适合你的场景,因为你的联合类型只有null和InterestSubmitParam两个选项,关闭类型信息不会有歧义。
方法二:手动遍历GenericRecord,构建ObjectNode
如果你的Avro版本不支持上述配置,或者需要更灵活的转换逻辑,可以手动遍历GenericRecord的字段,逐个构建ObjectNode,遇到嵌套的GenericRecord就递归处理:
import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.avro.generic.GenericData; import org.apache.avro.generic.GenericRecord; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; private static ObjectNode convertGenericRecordToObjectNode(GenericRecord record, ObjectMapper objectMapper) { ObjectNode node = objectMapper.createObjectNode(); for (String fieldName : record.getSchema().getFieldNames()) { Object value = record.get(fieldName); if (value == null) { node.putNull(fieldName); continue; } // 处理嵌套的自定义记录 if (value instanceof GenericRecord) { node.set(fieldName, convertGenericRecordToObjectNode((GenericRecord) value, objectMapper)); } else if (value instanceof GenericData.Array) { // 处理数组类型 GenericData.Array<?> array = (GenericData.Array<?>) value; JsonNode arrayNode = objectMapper.valueToTree(array); node.set(fieldName, arrayNode); } else { // 处理字符串、数字等基本类型 node.putPOJO(fieldName, value); } } return node; } // 使用示例 GenericRecord genericRecord = ...; ObjectMapper objectMapper = new ObjectMapper(); ObjectNode resultNode = convertGenericRecordToObjectNode(genericRecord, objectMapper); String jsonResult = objectMapper.writeValueAsString(resultNode);
这种方式完全由你掌控转换逻辑,不用担心Avro编码器的默认行为干扰。
内容的提问来源于stack exchange,提问作者Viet Phan
相关产品推荐
相关产品推荐

