jacksonその2:TreeModel
jacksonについて調べたときのメモその2です。
環境は前回の記事と同じですので省略します。
https://baubaubau.hatenablog.com/entry/2024/02/04/145625
TreeModelというものがどうやらあるらしいので触ってみましょう。
https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#tree-model
一応wikiにもページがありましたが中身が空っぽのようです。
https://github.com/FasterXML/jackson-databind/wiki/JacksonTreeModel
TreeModel
jacksonを使うとJavaのオブジェクトとJSONまたは、ListやMapなどのコレクションなどとJSONとの間で相互変換ができますが、JSONをトラバーサルするのには向いていません。ということでTreeModelというものがあるぞ、というみたいです。
ObjectMapperをインスタンス化し、
ObjectMapper objectMapper = new ObjectMapper();
readTreeでJSON文字列を読み込みます。するとJSONのルート要素を表現するJsonNodeが返ってくるようです。
JsonNode root; root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25 }, "type": [ "software engineer" ] } } """);
トラバーサルして値を読み込むにはgetを使い目的のプロパティまで掘っていけば良さそう。
値の型に応じてasなんちゃらメソッドを呼び出せば対応するJavaの値で取得できます。
String firstName = root.get("NB001").get("profile").get("firstName").asText(); assertEquals("book", firstName); int age = root.get("NB001").get("profile").get("age").asInt(); assertEquals(25, age);
値がない場合、結果はnullになりました。
JsonNode notExist = root.get("notExist");
assertNull(notExist);
値と型が適切ではない場合も試してみました。
どうやら変換できるものは頑張ってくれますが、無理なものは無意味な値で取得されるようです。てっきり例外が発生するかと思いましたがそうではないみたい。
int firstName = root.get("NB001").get("profile").get("firstName").asInt(); assertEquals(0, firstName); // 本当は "book" String age = root.get("NB001").get("profile").get("age").asText(); assertEquals("25", age); // 数値の25が文字列に変換された
値を取得する前に対象がどんな型なのかを確認するには、isなんちゃらのメソッドを使えば良さそうです。
JsonNode firstNameNode = root.get("NB001").get("profile").get("firstName"); assertTrue(firstNameNode.isTextual()); assertFalse(firstNameNode.isInt()); JsonNode ageNode = root.get("NB001").get("profile").get("age"); assertTrue(ageNode.isInt()); assertFalse(ageNode.isTextual());
トラバーサるしつつ、JSONの一部分をオブジェクトへマッピングできるようです。
次のようなクラスを定義し、
public record Profile(String firstName, String lastName, int age) { }
treeToValueで読み込みます。
Profile profile = objectMapper.treeToValue(root.get("NB001").get("profile"), Profile.class); assertEquals(new Profile("book", "store", 25), profile);
そういえば、jacksonはネストされた構造のJSONを読み込むとどうなるのでしょうか?
こういうクラスを用意し、
record ProfileNestedJson(String firstName, String lastName, int age, ProfileNestedJsonAddress address) {
}
record ProfileNestedJsonAddress(String country, String countryCode) {
}
以下のように読み込めました。
JsonNode root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25, "address": { "country": "Japan", "countryCode": "JPN" } }, "type": [ "software engineer" ] } } """); var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class); assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);
Javaのクラスにはないプロパティを持つJSONはどうなるのでしょうか?
次のようにpostalCodeを追加してJSONを読み込むと例外が発生しました。てっきり無視されるかと思いましたが。
JsonNode root = objectMapper.readTree(""" { "NB001": { "profile": { "firstName": "book", "lastName": "store", "age": 25, "address": { "country": "Japan", "countryCode": "JPN", "postalCode": "000-000" } }, "type": [ "software engineer" ] } } """); var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJson.class); assertEquals(new ProfileNestedJson("book", "store", 25, new ProfileNestedJsonAddress("Japan", "JPN")), profileNestedJson);
例外は次の通り。丁寧にignorableじゃないぞと書かれています。
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "postalCode" (class org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress), not marked as ignorable (2 known properties: "countryCode", "country"])
at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: org.example.TreeModelTest$TreeToValueTest$ProfileNestedJson["address"]->org.example.TreeModelTest$TreeToValueTest$ProfileNestedJsonAddress["postalCode"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61)
at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1153)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2224)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1719)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1697)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:279)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:464)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:545)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeWithErrorWrapping(BeanDeserializer.java:571)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeUsingPropertyBased(BeanDeserializer.java:440)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1419)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:348)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:185)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:342)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4875)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3033)
at com.fasterxml.jackson.databind.ObjectMapper.treeToValue(ObjectMapper.java:3497)
at org.example.TreeModelTest$TreeToValueTest.treeToValueNestedJsonAdditionalProperties(TreeModelTest.java:157)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
どうやら @JsonIgnoreProperties で無視するプロパティを指定する必要があるとのこと。
https://github.com/FasterXML/jackson-databind?tab=readme-ov-file#annotations-ignoring-properties
というわけで次のようにしたら動きました。
record ProfileNestedJsonAdditionalProperties(String firstName,
String lastName,
int age,
ProfileNestedJsonAdditionalPropertiesAddress address) {
}
@JsonIgnoreProperties("postalCode")
record ProfileNestedJsonAdditionalPropertiesAddress(String country, String countryCode) {
}
JsonNode root = objectMapper.readTree("""
{
"NB001": {
"profile": {
"firstName": "book",
"lastName": "store",
"age": 25,
"address": {
"country": "Japan",
"countryCode": "JPN",
"postalCode": "000-000"
}
},
"type": [
"software engineer"
]
}
}
""");
var profileNestedJson = objectMapper.treeToValue(root.get("NB001").get("profile"), ProfileNestedJsonAdditionalProperties.class);
assertEquals(new ProfileNestedJsonAdditionalProperties("book", "store", 25,
new ProfileNestedJsonAdditionalPropertiesAddress("Japan", "JPN")), profileNestedJson);