【Java】Jacksonで命名規則の異なるJSONをオブジェクトにバインドする方法

はじめに

 JSONをオブジェクトにバインドするライブラリとしてまず浮かぶのが、JacksonのObjectMapperです。

 Jacksonでバインドする場合、JSONのKeyとBeanのフィールドは正確に合わせないと、UnrecognizedPropertyExceptionなどで弾かれてうまくバインドできません。

 しかし、JSONで取得できるデータの命名規則はさまざまです。スネークケース(snake_case)の場合もあれば、キャメル(camelCase)、パスカル(PascalCase)、大文字のスネークケース(SNAKE_CASE)ということもあります。

 これらを正しくバインドする方法を、いくつかご紹介します。

JSONからBeanへの変換

@JsonPropertyを使う

 最も単純かつ簡単な方法が、@JsonPropertyアノテーションを使う方法です。

@Data
public class User{

  @JsonProperty("user_id")
  private int userId;

  @JsonProperty("user_name")
  private String userName;
}

public class Main{
  public static void main(String[] args){
    ObjectMapper om = new ObjectMapper();
    User user = om.readValue("{\"user_id\":1, \"userName\":\"John\"}", User.class);
    System.out.println(user.getUserId());      // -> 1
    System.out.println(user.getUserName());    // -> John
  }
}

 しかし、これは多くのbean定義を必要とする場合において適切な方法ではありません。単純に手間がかかりますし、たとえばJSON側の命名規則が変更された場合に、すべてのBeanをメンテナンスする必要があります。

 特別な理由がない限り、命名規則のギャップを埋めるために@JsonPropertyは使わないほうがよさそうです。

PropertyNamingStrategyを使う

 命名規則の違い程度であれば、PropertyNamingStrategyクラスを活用することで、簡単に実現できます。

@Data
public class User{

  private int userId;

  private String userName;
}

public class Main{
  public static void main(String[] args){
    ObjectMapper om = new ObjectMapper();
    User user = om.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE).readValue("{\"user_id\":1, \"userName\":\"John\"}", User.class);
    System.out.println(user.getUserId());      // -> 1
    System.out.println(user.getUserName());    // -> John
  }
}

PropertyNamingStrategyを継承する

 しかし、世の中には妙な命名規則を持ったJSONが存在していたりします。PHPやRailsなどで見られるスネークケースや、パスカル、キャメルなどはポピュラーですが、Javaでは定数として扱われるアッパースネークケースなどはあまりメジャーではありませんよね。

 また、たとえばbem記法などを参考にしてアンダースコア2つ繋ぎのものも出てきたりするかもしれません。非常にレアなケースですが、これらに対応することも勿論可能です。

アッパースネークケース(SNAKE_CASE)の場合

@Data
public class User{

  private int userId;

  private String userName;
}

public class UpperSnakeStrategy extends PropertyNamingStrategy{

  private static final long serialVersionUID = 1L;

  @Override
  public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName){
    // Google Guavaを利用してlowerCamelをUpperSnakeCaseに変換
    return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, defaultName);
  }

}

public class Main{
  public static void main(String[] args){
    ObjectMapper om = new ObjectMapper();
    User user = om.setPropertyNamingStrategy(new UpperSnakeStrategy()).readValue("{\"USER_ID\":1, \"USER_NAME\":\"John\"}", User.class);
    System.out.println(user.getUserId());      // -> 1
    System.out.println(user.getUserName());    // -> John
  }
}

アンダースコア2つの場合

@Data
public class User{

  private int userId;

  private String userName;
}

public class UpperSnakeStrategy extends PropertyNamingStrategy{

  private static final long serialVersionUID = 1L;

  @Override
  public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName){
    // Google Guavaを利用してlowerCamelをUpperSnakeCaseに変換し、かつ_を__に置換
    return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, defaultName).replace("_", "__");
  }

}

public class Main{
  public static void main(String[] args){
    ObjectMapper om = new ObjectMapper();
    User user = om.setPropertyNamingStrategy(new UpperSnakeStrategy()).readValue("{\"USER__ID\":1, \"USER__NAME\":\"John\"}", User.class);
    System.out.println(user.getUserId());      // -> 1
    System.out.println(user.getUserName());    // -> John
  }
}

BeanからJSONへの変換

 @JsonPropertyや、PropertyNamingStrategyの定数を使う場合は、上で挙げたコードのreadValuewriteValueAs〇〇に変更するだけで問題なく使えるはずです。

 ただし、PropertyNamingStrategyを継承する場合、上記に加えてnameForGetterMethodのオーバーライドが必要になります。

@Data
public class User{

  private int userId;

  private String userName;
}

public class UpperSnakeStrategy extends PropertyNamingStrategy{

  private static final long serialVersionUID = 1L;

  @Override
  public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName){
    return convert(defaultName);
  }

  @Override
  public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName){
    return convert(defaultName);
  }

  private String convert(String str){
    // Google Guavaを利用してlowerCamelをUpperSnakeCaseに変換
    return CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, str);
  }

}

public class Main{
  public static void main(String[] args){
    ObjectMapper om = new ObjectMapper();
    User user = om.setPropertyNamingStrategy(new UpperSnakeStrategy()).readValue("{\"USER_ID\":1, \"USER_NAME\":\"John\"}", User.class);
    System.out.println(user.getUserId());      // -> 1
    System.out.println(user.getUserName());    // -> John
    System.out.println(om.writeValueAsString(user));    // -> {"USER_ID":1, "USER_NAME":"John"}
  }
}

PropertyNamingStrategyの、その他のオーバーライド可能なメソッド

 上記で挙げた他に、コンストラクタの引数を変換するためのnameForConstructorParameterメソッドや、フィールドそのものを変換するためのnameForFieldメソッドがあります。

 単純なBean以外のモデルを作成するときに使えそうです。

まとめ

 本例以外にも、ライブラリのクラスを継承して固有機能を持ったクラスを作ることは多くあります。新しいライブラリを使って、「機能が足りないな…」と思ったら、内部仕様やドキュメントを確認して部品を独自実装してみるのもよいでしょう。