Closed6

Jackson の勉強 20210908

bufferingsbufferings

https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations

https://www.baeldung.com/Jackson-annotations/

プロパティ名

  • @JsonProperty 違う名前をつけられる

対象プロパティ

  • @JsonAutoDetect プロパティの対象 Visibility を設定できる。private フィールドを対象にするとか
  • @JsonIgnore プロパティを無視するのを設定できる。基本的にはプロパティ全体に適用される(例えば Getter に指定するとそのプロパティの Setter も無視される)けど、Setter に @JsonProperty を指定すると read-only にできる(読み込むけど書き込まない)
  • @JsonIgnoreProperties クラスに対するアノテーション。無視するプロパティのリストを設定することができる。ignoreUnknown=true を指定するとデシリアライズするときに、知らないプロパティを無視する
  • @JsonIgnoreType クラスに対するアノテーション。指定したタイプのプロパティを無視する
  • @JsonInclude シリアライズするときに null や空のプロパティを含めるかどうかを指定。プロパティまたはクラスに指定できる

フォーマット

  • @JsonFormat Date/Time をシリアライズするフォーマットを指定
  • @JsonUnwrapped シリアライズするときに、内部に持ってる構造をアンラップする。デシリアライズするときはラップする

分からんからやってみた

public class JsonUnwrappedTest {

  public static class UnwrappedUser {
    public int id;

    @JsonUnwrapped
    public Name name;

    public UnwrappedUser() {
    }

    public UnwrappedUser(int id, Name name) {
      this.id = id;
      this.name = name;
    }

    public static class Name {
      public String firstName;
      public String lastName;

      public Name() {
      }

      public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
      }
    }
  }

  @Test
  public void unwrapWhenSerializing() throws Exception {
    UnwrappedUser.Name name = new UnwrappedUser.Name("John", "Doe");
    UnwrappedUser user = new UnwrappedUser(1, name);

    String result = new ObjectMapper().writeValueAsString(user);

    String expected = """
        {"id":1,"firstName":"John","lastName":"Doe"}""";
    assertEquals(expected, result);
  }

  @Test
  public void wrapWhenDeserializing()
      throws Exception {
    String json = """
        {"id":1,"firstName":"John","lastName":"Doe"}""";

    UnwrappedUser result = new ObjectMapper().readValue(json, UnwrappedUser.class);

    assertEquals(1, result.id);
    assertEquals("John", result.name.firstName);
    assertEquals("Doe", result.name.lastName);
  }
}

へー。

  • @JsonView どのフィールドをシリアライズするかを紐付けることができる

分からんからやってみた

public class JsonViewTest {

  public static class Views {
    public static class Public {}

    public static class Internal extends Public {}
  }

  public static class Item {

    public Item() {
    }

    public Item(int id, String itemName, String ownerName, String creator) {
      this.id = id;
      this.itemName = itemName;
      this.ownerName = ownerName;
      this.creator = creator;
    }

    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String itemName;

    @JsonView(Views.Internal.class)
    public String ownerName;

    public String creator;
  }

  @Test
  public void withParentView() throws Exception {
    Item item = new Item(2, "book", "John", "Mike");

    String result = new ObjectMapper()
        .writerWithView(Views.Public.class)
        .writeValueAsString(item);

    String expected = """
        {"id":2,"itemName":"book","creator":"Mike"}""";
    assertEquals(expected, result);
  }

  @Test
  public void withChildView() throws Exception {
    Item item = new Item(2, "book", "John", "Mike");

    String result = new ObjectMapper()
        .writerWithView(Views.Internal.class)
        .writeValueAsString(item);

    String expected = """
        {"id":2,"itemName":"book","ownerName":"John","creator":"Mike"}""";
    assertEquals(expected, result);
  }

}

へー。

ちょっと休憩

bufferingsbufferings

デシリアライズ関係

@JacksonInject

  • 指定したプロパティを JSON からじゃなくて、インジェクションから設定する

@JsonAnySetter

  • key と value を受け取るメソッドに指定して、マッピングできないプロパティを受け取る
public class JsonAnySetterTest {
  public static class MyItem {
    public String name;
    public Map<String, String> properties = new HashMap<>();

    @JsonAnySetter
    public void add(String key, String value) {
      properties.put(key, value);
    }
  }

  @Test
  public void test() throws Exception {
    String json = """
        {"id":2,"name":"book","ownerName":"John","creator":"Mike"}""";

    MyItem result = new ObjectMapper().readValue(json, MyItem.class);

    assertEquals("book", result.name);
    assertEquals("2", result.properties.get("id"));
    assertEquals("John", result.properties.get("ownerName"));
    assertEquals("Mike", result.properties.get("creator"));
  }
}

@JsonCreator

  • デシリアライズに使ってほしいコンストラクターやファクトリーメソッドにつける

@JsonSetter

  • @JsonProperty の代わり。Setter に対して使う。JSON とクラスでプロパティ名が異なるときに便利

@JsonEnumDefaultValue

  • Enum の値が知らない値の場合に使う値を設定
public class JsonEnumDefaultValueTest {
  enum Types {
    TYPE_A, TYPE_B, @JsonEnumDefaultValue TYPE_C
  }

  @Test
  public void test() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    mapper.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE);

    assertEquals(TYPE_A, mapper.readValue("\"TYPE_A\"", Types.class));
    assertEquals(TYPE_B, mapper.readValue("\"TYPE_B\"", Types.class));
    assertEquals(TYPE_C, mapper.readValue("\"TYPE_C\"", Types.class));

    assertEquals(TYPE_C, mapper.readValue("\"TYPE_UNKNOWN\"", Types.class));
  }
}

シリアライズ関係

@JsonAnyGetter

  • @JsonAnySetter の Getter 版。Map を返すメソッドにつけて、それを JSON に出力する

@JsonGetter

  • @JsonSetter の Getter 版。@JsonProperty の代わり

@JsonPropertyOrder

  • クラスにつけてプロパティの出力順を指定できる。アルファベット順もできる
  • プロパティにも指定できるけど、それは主に Map プロパティ用

@JsonRawValue

  • プロパティにつけると、値をエスケープしたりせずにそのまま出力することができる

@JsonValue

  • そのクラスのシリアライズに使うメソッドを指定できる。Enum で異なる文字列表現を使いたい場合などに使える

@JsonRootName

  • root-wrapping が有効な場合にラッピングに使われる名前を指定
public class JsonRootNameTest {
  @JsonRootName(value = "my_item")
  public static class MyItem {
    public String name;
    public String ownerName;

    public MyItem(String name, String ownerName) {
      this.name = name;
      this.ownerName = ownerName;
    }
  }

  @Test
  public void test() throws Exception {
    MyItem myItem = new MyItem("Book", "Bob");

    ObjectMapper mapper = new ObjectMapper();
    mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
    String result = mapper.writeValueAsString(myItem);

    String expect = """
        {"my_item":{"name":"Book","ownerName":"Bob"}}""";
    assertEquals(expect, result);
  }
}

タイプ関係

インターフェースとか継承とか使ってるときのやつ

  • @JsonTypeInfo
    • クラスまたはプロパティにつける
    • シリアライズするときにタイプの情報をどんなふうに埋め込むかを決める
  • @JsonSubTypes
    • クラスにつける
    • サブタイプの情報。クラス名を使いたくないときに使える
  • @JsonTypeName
    • クラスにつける
    • タイプの情報
  • @JsonTypeId
    • プロパティにつける
    • これをオブジェクトのタイプIDとして使う

ふむ。分からん。ちょっとあとで別でやろっと。

参照関係

@JsonManagedReference と `@JsonBackReference

  • プロパティの相互参照のループを防ぐことができる
  • →メモ:使いたいときに調べたら良いや

@JsonIdentityInfo

  • オブジェクトのIDを指定することで循環参照してるやつとかに対応できる
  • →メモ:使いたいときに調べたら良いや

その他

@JsonAlias

  • プロパティにつけて、デシリアライズするときに1つ以上の別の名前から読み込むことができる。シリアライズするときには、このアノテーションの情報ではなく、プロパティの名前が使われる。

@JsonFilter

  • ObjectMapper に登録しておいたフィルターのIDを指定して、出力するプロパティをフィルタリングできる

@JsonMerge

  • 既存オブジェクトを JSON の情報を元に更新するときに、内部のオブジェクトを入れ替えるんじゃなくて更新するようになる

@JsonKey

@JsonIncludeProperties

bufferingsbufferings

んで、Polymorphic Type のところやってみよう

安心のぼぼさんとこ見ながら

https://qiita.com/opengl-8080/items/b613b9b3bc5d796c840c#型情報が

なるほどー。インターフェースとか継承とか使ってるときに、その親クラスを使ってデシリアライズしようとすると例外になるのか:

  public static abstract class Animal {
    public String name;
  }

  public static class Dog extends Animal {
    public double barkVolume;
  }

  public static class Cat extends Animal {
    public boolean likesCream;
    public int lives;
  }

  @Test
  public void cannotDeserializeDogAsAnimal() throws Exception {
    String json = """
        {"name":"Hachi","barkVolume":20.0}""";

    var mapper = new ObjectMapper();
    assertThrows(InvalidDefinitionException.class, () -> {
      mapper.readValue(json, Animal.class);
    });
  }

だから @JsonTypeInfo を使って、シリアライズのときにクラス情報を出すようにすれば、親クラスを使ってもデシリアライズできるようになる:

  @JsonTypeInfo(use = Id.CLASS)
  public static abstract class Animal {
    public String name;
  }

  public static class Dog extends Animal {
    public double barkVolume;
  }

  public static class Cat extends Animal {
    public boolean likesCream;
    public int lives;
  }

  @Test
  public void canDeserializeDogAsAnimalWithClass() throws Exception {
    String json = """
        {"@class":"com.example.learningjackson.PolymorphicType2Test$Dog","name":"Hachi","barkVolume":20.0}""";

    var mapper = new ObjectMapper();
    Animal animal = mapper.readValue(json, Animal.class);

    if (!(animal instanceof Dog dog)) {
      fail("Animal isn't dog");
      return;
    }

    assertEquals("Hachi", dog.name);
    assertEquals(20.0, dog.barkVolume, 0);
  }

@class のところにクラス名が入ってる。

クラス名を JSON に出すのは嫌だなーってときは @JsonTypeInfo(use = Id.NAME) を指定して @JsonSubTypes で名前をつけてあげればOK:

  @JsonTypeInfo(use = Id.NAME)
  @JsonSubTypes({
      @Type(value = Dog.class, name = "dog"),
      @Type(value = Cat.class, name = "cat")
  })
  public static abstract class Animal {
    public String name;
  }

  public static class Dog extends Animal {
    public double barkVolume;
  }

  public static class Cat extends Animal {
    public boolean likesCream;
    public int lives;
  }

  @Test
  public void canDeserializeDogAsAnimal() throws Exception {
    String json = """
        {"@type":"dog","name":"Hachi","barkVolume":20.0}""";

    var mapper = new ObjectMapper();
    Animal animal = mapper.readValue(json, Animal.class);

    if (!(animal instanceof Dog dog)) {
      fail("Animal isn't dog");
      return;
    }

    assertEquals("Hachi", dog.name);
    assertEquals(20.0, dog.barkVolume, 0);
  }

今度は @type で出てる。

親クラスで名前までつけてしまうのはちょっとなーってときは、子クラス側で @JsonTypeName を使って指定できる:

  @JsonTypeInfo(use = Id.NAME)
  @JsonSubTypes({
      @Type(Dog.class),
      @Type(Cat.class)
  })
  public static abstract class Animal {
    public String name;
  }

  @JsonTypeName("dog")
  public static class Dog extends Animal {
    public double barkVolume;
  }

  @JsonTypeName("cat")
  public static class Cat extends Animal {
    public boolean likesCream;
    public int lives;
  }

子クラスで @JsonTypeName を指定してるから、親クラスの @JsonSubTypes は要らなくなるのかなぁって思って外したらエラーになった。

Javadoc にこう書いてあった:

http://fasterxml.github.io/jackson-annotations/javadoc/2.12/com/fasterxml/jackson/annotation/JsonSubTypes.Type.html

Definition of a subtype, along with optional name(s). If no name is defined (empty Strings are ignored), class of the type will be checked for JsonTypeName annotation; and if that is also missing or empty, a default name will be constructed by type id mechanism. Default name is usually based on class name.

@JsonSubTypes で名前を指定してなかったら @JsonTypeName を見にいってそれを使う。もしそれも指定されてなかったら、クラス名から作ったデフォルト名を使う。ってことか。

あくまでも名前だけを子クラス側でつけれるよってことか。まぁそうか。Java 17 の Sealed Class が入ってきたら、子クラスが限定されるから多少変わるかもしれないね。

それからもう一つ、2.12 で追加された機能。DEDUCTION

https://cowtowncoder.medium.com/jackson-2-12-most-wanted-1-5-deduction-based-polymorphism-c7fb51db7818

フィールドに違いがある場合は type 情報なしでもいい感じにやってくれる:

  @JsonTypeInfo(use = Id.DEDUCTION)
  @JsonSubTypes({
      @Type(Dog.class),
      @Type(Cat.class)
  })
  public static abstract class Animal {
    public String name;
  }

  public static class Dog extends Animal {
    public double barkVolume;
  }

  public static class Cat extends Animal {
    public boolean likesCream;
    public int lives;
  }

  @Test
  public void canDeserializeDogAsAnimal() throws Exception {
    String json = """
        {"name":"Hachi","barkVolume":20.0}""";

    var mapper = new ObjectMapper();
    Animal animal = mapper.readValue(json, Animal.class);

    if (!(animal instanceof Dog dog)) {
      fail("Animal isn't dog");
      return;
    }

    assertEquals("Hachi", dog.name);
    assertEquals(20.0, dog.barkVolume, 0);
  }

JSON に type 情報が出ないのはスッキリしてていいね。フィールドの有無によって動きが変わることになるからちょっとムズムズするけど。

http://fasterxml.github.io/jackson-annotations/javadoc/2.12/com/fasterxml/jackson/annotation/JsonTypeInfo.Id.html#DEDUCTION

Means that no serialized typing-property is used. Types are deduced based on the fields available. Deduction is limited to the names of fields (not their values or, consequently, any nested descendants). Exceptions will be thrown if not enough unique information is present to select a single subtype. If deduction is being used annotation properties visible, property and include are ignored.
Since:
2.12.0.

今回はこんなところかな。色々面白かった。また気が向いたら触ってみよっと。

このスクラップは2021/09/09にクローズされました