使用fasterxml.jackson将JSON解析为Java记录

问题描述 投票:0回答:2

根据设计,Java 记录不能从另一个对象继承(请参阅为什么 Java 记录不支持继承?)。所以我想知道实现以下目标的最佳方法是什么。

鉴于我的 JSON 数据包含具有一些通用数据 + 独特数据的对象。例如,类型、宽度和高度可以是所有形状,但根据类型,它们可以具有附加字段:

{
  "name": "testDrawing",
  "shapes": [
    {
      "type": "shapeA",
      "width": 100,
      "height": 200,
      "label": "test"
    },
    {
      "type": "shapeB",
      "width": 100,
      "height": 200,
      "length": 300
    },
    {
      "type": "shapeC",
      "width": 100,
      "height": 200,
      "url": "www.test.be",
      "color": "#FF2233"
    }
  ]
}

在“传统”Java 中,您可以使用

BaseShape with width and height
ShapeA extends BaseShape with label
ShapeB extends BaseShape with length
ShapeC extends BaseShape with URL and color

但是我有点固执,真的很想用记录。

我的解决方案现在看起来像这样:

  • 无基础形状
  • 公共字段在所有记录中重复
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Drawing(
        @JsonProperty("name")
        String name,

        @JsonProperty("shapes")
        @JsonDeserialize(using = TestDeserializer.class)
        List<Object> shapes // I don't like the Objects here... 
) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeA (
        @JsonProperty("type") String type,
        @JsonProperty("width") Integer width,
        @JsonProperty("height") Integer height,
        @JsonProperty("label") String label
) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeB(
        @JsonProperty("type") String type,
        @JsonProperty("width") Integer width,
        @JsonProperty("height") Integer height,
        @JsonProperty("length") Integer length
) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeC(
        @JsonProperty("type") String type,
        @JsonProperty("width") Integer width,
        @JsonProperty("height") Integer height,
        @JsonProperty("url") String url,
        @JsonProperty("color") String color
) {
}

我不喜欢重复的代码,这是一种不好的做法...但最终我可以使用这个辅助类来加载它:

public class TestDeserializer extends JsonDeserializer {

    ObjectMapper mapper = new ObjectMapper();

    @Override
    public List<Object> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        List<Object> rt = new ArrayList<>();

        JsonNode node = jsonParser.getCodec().readTree(jsonParser);

        if (node instanceof ArrayNode array) {
            for (Iterator<JsonNode> it = array.elements(); it.hasNext(); ) {
                JsonNode childNode = it.next();
                rt.add(getShape(childNode));
            }
        } else {
            rt.add(getShape(node));
        }

        return rt;
    }

    private Object getShape(JsonNode node) {
        var type = node.get("type").asText();
        switch (type) {
            case "shapeA":
                return mapper.convertValue(node, ShapeA.class);
            case "shapeB":
                return mapper.convertValue(node, ShapeB.class);
            case "shapeC":
                return mapper.convertValue(node, ShapeC.class);
            default:
                throw new IllegalArgumentException("Shape could not be parsed");
        }
    }
}

这个测试证明工作正常:

@Test
    void fromJsonToJson() throws IOException, JSONException {
        File f = new File(this.getClass().getResource("/test.json").getFile());
        String jsonFromFile = Files.readString(f.toPath());

        ObjectMapper mapper = new ObjectMapper();
        Drawing drawing = mapper.readValue(jsonFromFile, Drawing.class);
        String jsonFromObject = mapper.writeValueAsString(drawing);

        System.out.println("Original:\n" + jsonFromFile.replace("\n", "").replace(" ", ""));
        System.out.println("Generated:\n" + jsonFromObject);

        assertAll(
                //() -> assertEquals(jsonFromFile, jsonFromObject),
                () -> assertEquals("testDrawing", drawing.name()),
                () -> assertTrue(drawing.shapes().get(0) instanceof ShapeA),
                () -> assertTrue(drawing.shapes().get(1) instanceof ShapeB),
                () -> assertTrue(drawing.shapes().get(2) instanceof ShapeC)
        );
    }

使用 Jackson 库和 Java Records 实现这一目标的最佳方法是什么?

额外旁注:我还需要能够以与原始格式相同的格式写回 JSON。

java json jackson record
2个回答
6
投票

记录不能继承,因为它们旨在成为一个可靠的契约,但它们可以实现一个接口。因此,您可以使用 Jackson 2.12 或更高版本使用

JasonSubTypes
执行类似的操作:

型号

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record Drawing(
        String name,
        List<BaseShape> shapes
) { }

// added benefit of interface here is it reminds you to have the default fields
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
        @JsonSubTypes.Type(ShapeA.class),
        @JsonSubTypes.Type(ShapeB.class),
        @JsonSubTypes.Type(ShapeC.class)
})
public interface BaseShape {
    Integer width();
    Integer height();
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeA (
        Integer width,
        Integer height,
        String label
) implements BaseShape { }

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeB(
        Integer width,
        Integer height,
        Integer length
) implements BaseShape { }

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ShapeC(
        Integer width,
        Integer height,
        String url,
        String color
) implements BaseShape { }

测试班

@Slf4j
class DemoTest {

    private ObjectMapper objectMapper = ObjectMapperBuilder.getObjectMapper();

    @Test
    void test() throws JsonProcessingException {
        final String testString = objectMapper
                .writerWithDefaultPrettyPrinter()
                .writeValueAsString(
                        new Drawing(
                                "happy",
                                List.of(
                                        new ShapeA(1, 1, "happyShape"),
                                        new ShapeB(2, 2, 3),
                                        new ShapeC(2, 2, "www.shape.com/shape", "blue"
                                        )
                                )
                        )
                );

        log.info("From model to string {}", testString);

        Drawing drawing = objectMapper.readValue(testString, Drawing.class);

        log.info(
                "Captured types {}",
                drawing
                    .shapes()
                    .stream()
                    .map(s -> s.getClass().getName())
                    .collect(Collectors.toSet())
        );

        log.info(
                "From string back to model then again to string {}",
                objectMapper
                    .writerWithDefaultPrettyPrinter()
                    .writeValueAsString(drawing)
        );
    }

}

这是测试日志输出:

17:06:41.293 [Test worker] INFO com.demo.DemoTest - From model to string {
  "name" : "happy",
  "shapes" : [ {
    "width" : 1,
    "height" : 1,
    "label" : "happyShape"
  }, {
    "width" : 2,
    "height" : 2,
    "length" : 3
  }, {
    "width" : 2,
    "height" : 2,
    "url" : "www.shape.com/shape",
    "color" : "blue"
  } ]
}
17:06:41.353 [Test worker] INFO com.demo.DemoTest - Captured types [com.demo.DemoTest$ShapeB, com.demo.DemoTest$ShapeA, com.demo.DemoTest$ShapeC]
17:06:41.354 [Test worker] INFO com.demo.DemoTest - From string back to model then again to string {
  "name" : "happy",
  "shapes" : [ {
    "width" : 1,
    "height" : 1,
    "label" : "happyShape"
  }, {
    "width" : 2,
    "height" : 2,
    "length" : 3
  }, {
    "width" : 2,
    "height" : 2,
    "url" : "www.shape.com/shape",
    "color" : "blue"
  } ]
}

请注意,您可以将

type
字段添加为
name
注释的
@JsonSubTypes.Type
属性,但是只要记录中的字段不完全相同,无论有或没有鉴别器,都可以使用。

您可以在此处

阅读更多有关JsonSubtypes的信息。


-1
投票

伊戈尔的回答非常好!谢谢...

© www.soinside.com 2019 - 2024. All rights reserved.