その他のJackson注釈

その他のジャクソン注釈

1. 概要

この記事では、前の記事A Guide to Jackson Annotationsではカバーされていなかったいくつかの追加の注釈について説明します。これらのうち7つについて説明します。

2. @JsonIdentityReference

@JsonIdentityReferenceは、完全なPOJOではなくオブジェクトIDとしてシリアル化されるオブジェクトへの参照のカスタマイズに使用されます。 これは@JsonIdentityInfoと連携して機能し、@JsonIdentityReferenceが存在しない場合とは異なり、すべてのシリアル化でオブジェクトIDの使用を強制します。 このアノテーションのカップルは、オブジェクト間の循環依存関係を扱うときに最も役立ちます。 詳細については、Jackson – Bidirectional Relationshipの記事のセクション4を参照してください。

@JsonIdentityReferenceの使用法を示すために、このアノテーションの有無にかかわらず、2つの異なるBeanクラスを定義します。

@JsonIdentityReferenceのないBean:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class BeanWithoutIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

@JsonIdentityReferenceを使用するBeanの場合、オブジェクトIDとしてidプロパティを選択します。

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
@JsonIdentityReference(alwaysAsId = true)
public class BeanWithIdentityReference {
    private int id;
    private String name;

    // constructor, getters and setters
}

@JsonIdentityReferenceが存在しない最初のケースでは、そのBeanは、そのプロパティの完全な詳細とともにシリアル化されます。

BeanWithoutIdentityReference bean
  = new BeanWithoutIdentityReference(1, "Bean Without Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);

上記のシリアル化の出力:

{
    "id": 1,
    "name": "Bean Without Identity Reference Annotation"
}

@JsonIdentityReferenceが使用される場合、Beanは代わりに単純なIDとしてシリアル化されます。

BeanWithIdentityReference bean
  = new BeanWithIdentityReference(1, "Bean With Identity Reference Annotation");
String jsonString = mapper.writeValueAsString(bean);
assertEquals("1", jsonString);

3. @JsonAppend

@JsonAppendアノテーションは、オブジェクトがシリアル化されるときに、通常のプロパティに加えて仮想プロパティをオブジェクトに追加するために使用されます。 これは、クラス定義を変更するのではなく、補足情報をJSON文字列に直接追加する場合に必要です。 たとえば、Beanのversionメタデータを対応するJSONドキュメントに挿入する方が、追加のプロパティを提供するよりも便利な場合があります。

次のように、@JsonAppendのないBeanがあると仮定します。

public class BeanWithoutAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

テストでは、@JsonAppendアノテーションがない場合、ObjectWriterオブジェクトに追加しようとしているにもかかわらず、シリアル化出力に補足のversionプロパティに関する情報が含まれていないことが確認されます。 :

BeanWithoutAppend bean = new BeanWithoutAppend(2, "Bean Without Append Annotation");
ObjectWriter writer
  = mapper.writerFor(BeanWithoutAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

シリアル化出力:

{
    "id": 2,
    "name": "Bean Without Append Annotation"
}

ここで、@JsonAppendで注釈が付けられたBeanがあるとします。

@JsonAppend(attrs = {
  @JsonAppend.Attr(value = "version")
})
public class BeanWithAppend {
    private int id;
    private String name;

    // constructor, getters and setters
}

前のテストと同様のテストで、@JsonAppendアノテーションが適用されると、シリアル化後に補足プロパティが含まれることが確認されます。

BeanWithAppend bean = new BeanWithAppend(2, "Bean With Append Annotation");
ObjectWriter writer
  = mapper.writerFor(BeanWithAppend.class).withAttribute("version", "1.0");
String jsonString = writer.writeValueAsString(bean);

そのシリアル化の出力は、versionプロパティが追加されたことを示しています。

{
    "id": 2,
    "name": "Bean With Append Annotation",
    "version": "1.0"
}

4. @JsonNaming

@JsonNamingアノテーションは、シリアル化のプロパティの命名戦略を選択するために使用され、デフォルトをオーバーライドします。 value要素を使用して、カスタム戦略を含む任意の戦略を指定できます。

デフォルトであるLOWER_CAMEL_CASEに加えて(例: lowerCamelCase)、Jacksonライブラリは、便利な他の4つの組み込みプロパティ命名戦略を提供します。

  • KEBAB_CASE:名前要素はハイフンで区切られます。例: kebab-case

  • LOWER_CASE:すべての文字は小文字で、区切り文字はありません。例: lowercase

  • SNAKE_CASE:すべての文字は小文字で、名前要素間の区切り文字としてアンダースコアが付いています。例: snake_case

  • UPPER_CAMEL_CASE:最初の要素を含むすべての名前要素は、大文字で始まり、その後に小文字が続き、区切り文字はありません。 UpperCamelCase

この例では、スネークケース名を使用してプロパティをシリアル化する方法を説明します。ここで、beanNameという名前のプロパティはbean_name.としてシリアル化されます。

Bean定義が与えられた場合:

@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class NamingBean {
    private int id;
    private String beanName;

    // constructor, getters and setters
}

以下のテストは、指定された命名規則が必要に応じて機能することを示しています。

NamingBean bean = new NamingBean(3, "Naming Bean");
String jsonString = mapper.writeValueAsString(bean);
assertThat(jsonString, containsString("bean_name"));

jsonString変数には、次のデータが含まれています。

{
    "id": 3,
    "bean_name": "Naming Bean"
}

5. @JsonPropertyDescription

Jacksonライブラリは、JSON Schemaと呼ばれる別のモジュールを使用して、JavaタイプのJSONスキーマを作成できます。 スキーマは、Javaオブジェクトをシリアル化するときに予想される出力を指定する場合、または逆シリアル化する前にJSONドキュメントを検証する場合に役立ちます。

@JsonPropertyDescriptionアノテーションを使用すると、descriptionフィールドを指定することで、作成されたJSONスキーマに人間が読める形式の説明を追加できます。

このセクションでは、以下で宣言されているBeanを使用して、@JsonPropertyDescriptionの機能を示します。

public class PropertyDescriptionBean {
    private int id;
    @JsonPropertyDescription("This is a description of the name property")
    private String name;

    // getters and setters
}

descriptionフィールドを追加してJSONスキーマを生成する方法を以下に示します。

SchemaFactoryWrapper wrapper = new SchemaFactoryWrapper();
mapper.acceptJsonFormatVisitor(PropertyDescriptionBean.class, wrapper);
JsonSchema jsonSchema = wrapper.finalSchema();
String jsonString = mapper.writeValueAsString(jsonSchema);
assertThat(jsonString, containsString("This is a description of the name property"));

ご覧のとおり、JSONスキーマの生成は成功しました。

{
    "type": "object",
    "id": "urn:jsonschema:com:example:jackson:annotation:extra:PropertyDescriptionBean",
    "properties":
    {
        "name":
        {
            "type": "string",
            "description": "This is a description of the name property"
        },

        "id":
        {
            "type": "integer"
        }
    }
}

6. @JsonPOJOBuilder

@JsonPOJOBuilderアノテーションは、命名規則がデフォルトと異なる場合にPOJOを回復するために、JSONドキュメントの逆シリアル化をカスタマイズするビルダークラスを構成するために使用されます。

次のJSON文字列を逆シリアル化する必要があるとします。

{
    "id": 5,
    "name": "POJO Builder Bean"
}

そのJSONソースは、POJOBuilderBeanのインスタンスを作成するために使用されます。

@JsonDeserialize(builder = BeanBuilder.class)
public class POJOBuilderBean {
    private int identity;
    private String beanName;

    // constructor, getters and setters
}

Beanのプロパティの名前は、JSON文字列のフィールドの名前とは異なります。 ここで@JsonPOJOBuilderが役に立ちます。

@JsonPOJOBuilderアノテーションには、次の2つのプロパティがあります。

  • buildMethodName:JSONフィールドをそのBeanのプロパティにバインドした後、予期されるBeanをインスタンス化するために使用される引数なしメソッドの名前。 デフォルト名はbuildです。

  • withPrefix:JSONとBeanのプロパティ間の一致を自動検出するための名前プレフィックス。 デフォルトのプレフィックスはwithです。

この例では、POJOBuilderBeanで使用される以下のBeanBuilderクラスを使用します。

@JsonPOJOBuilder(buildMethodName = "createBean", withPrefix = "construct")
public class BeanBuilder {
    private int idValue;
    private String nameValue;

    public BeanBuilder constructId(int id) {
        idValue = id;
        return this;
    }

    public BeanBuilder constructName(String name) {
        nameValue = name;
        return this;
    }

    public POJOBuilderBean createBean() {
        return new POJOBuilderBean(idValue, nameValue);
    }
}

上記のコードでは、createBeanと呼ばれるビルドメソッドとconstructプレフィックスを使用してプロパティを照合するように@JsonPOJOBuilderを構成しました。

Beanへの@JsonPOJOBuilderの適用は、次のように説明およびテストされています。

String jsonString = "{\"id\":5,\"name\":\"POJO Builder Bean\"}";
POJOBuilderBean bean = mapper.readValue(jsonString, POJOBuilderBean.class);

assertEquals(5, bean.getIdentity());
assertEquals("POJO Builder Bean", bean.getBeanName());

結果は、プロパティ名の不一致にもかかわらず、新しいデータオブジェクトがJSONソースから正常に再作成されたことを示しています。

7. @JsonTypeId

@JsonTypeIdアノテーションは、通常のプロパティとしてではなく、ポリモーフィック型情報を含めるときに、アノテーション付きプロパティをタイプIDとしてシリアル化する必要があることを示すために使用されます。 その多態性メタデータは、宣言されたスーパータイプではなく、シリアル化前と同じサブタイプのオブジェクトを再作成するために、逆シリアル化中に使用されます。

ジャクソンの継承の処理の詳細については、Inheritance in Jacksonのセクション2を参照してください。

次のようなBeanクラス定義があるとします。

public class TypeIdBean {
    private int id;
    @JsonTypeId
    private String name;

    // constructor, getters and setters
}

次のテストは、@JsonTypeIdが意図したとおりに機能することを検証します。

mapper.enableDefaultTyping(DefaultTyping.NON_FINAL);
TypeIdBean bean = new TypeIdBean(6, "Type Id Bean");
String jsonString = mapper.writeValueAsString(bean);

assertThat(jsonString, containsString("Type Id Bean"));

シリアル化プロセスの出力:

[
    "Type Id Bean",
    {
        "id": 6
    }
]

8. @JsonTypeIdResolver

@JsonTypeIdResolverアノテーションは、シリアル化および逆シリアル化のカスタムタイプIDハンドラーを示すために使用されます。 そのハンドラーは、JSONドキュメントに含まれるJavaタイプとタイプID間の変換を行います。

次のクラス階層を処理するときに、JSON文字列に型情報を埋め込むとします。

AbstractBeanスーパークラス:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.PROPERTY,
  property = "@type"
)
@JsonTypeIdResolver(BeanIdResolver.class)
public class AbstractBean {
    private int id;

    protected AbstractBean(int id) {
        this.id = id;
    }

    // no-arg constructor, getter and setter
}

FirstBeanサブクラス:

public class FirstBean extends AbstractBean {
    String firstName;

    public FirstBean(int id, String name) {
        super(id);
        setFirstName(name);
    }

    // no-arg constructor, getter and setter
}

LastBeanサブクラス:

public class LastBean extends AbstractBean {
    String lastName;

    public LastBean(int id, String name) {
        super(id);
        setLastName(name);
    }

    // no-arg constructor, getter and setter
}

これらのクラスのインスタンスは、BeanContainerオブジェクトにデータを設定するために使用されます。

public class BeanContainer {
    private List beans;

    // getter and setter
}

AbstractBeanクラスには@JsonTypeIdResolverの注釈が付けられていることがわかります。これは、カスタムTypeIdResolverを使用して、シリアル化にサブタイプ情報を含める方法と、そのメタデータを他のメタデータを利用する方法を決定することを示しています。ずっと。

型情報の包含を処理するリゾルバークラスを次に示します。

public class BeanIdResolver extends TypeIdResolverBase {

    private JavaType superType;

    @Override
    public void init(JavaType baseType) {
        superType = baseType;
    }

    @Override
    public Id getMechanism() {
        return Id.NAME;
    }

    @Override
    public String idFromValue(Object obj) {
        return idFromValueAndType(obj, obj.getClass());
    }

    @Override
    public String idFromValueAndType(Object obj, Class subType) {
        String typeId = null;
        switch (subType.getSimpleName()) {
        case "FirstBean":
            typeId = "bean1";
            break;
        case "LastBean":
            typeId = "bean2";
        }
        return typeId;
    }

    @Override
    public JavaType typeFromId(DatabindContext context, String id) {
        Class subType = null;
        switch (id) {
        case "bean1":
            subType = FirstBean.class;
            break;
        case "bean2":
            subType = LastBean.class;
        }
        return context.constructSpecializedType(superType, subType);
    }
}

最も注目すべき2つのメソッドはidFromValueAndTypetypeFromIdで、前者はPOJOをシリアル化するときに型情報を含める方法を示し、後者はそのメタデータを使用して再作成されたオブジェクトのサブタイプを決定します。

シリアル化と逆シリアル化の両方が適切に機能することを確認するために、完全な進行状況を検証するテストを作成しましょう。

まず、BeanコンテナとBeanクラスをインスタンス化してから、そのコンテナにBeanインスタンスを追加する必要があります。

FirstBean bean1 = new FirstBean(1, "Bean 1");
LastBean bean2 = new LastBean(2, "Bean 2");

List beans = new ArrayList<>();
beans.add(bean1);
beans.add(bean2);

BeanContainer serializedContainer = new BeanContainer();
serializedContainer.setBeans(beans);

次に、BeanContainerオブジェクトがシリアル化され、結果の文字列に型情報が含まれていることを確認します。

String jsonString = mapper.writeValueAsString(serializedContainer);
assertThat(jsonString, containsString("bean1"));
assertThat(jsonString, containsString("bean2"));

シリアル化の出力は次のとおりです。

{
    "beans":
    [
        {
            "@type": "bean1",
            "id": 1,
            "firstName": "Bean 1"
        },

        {
            "@type": "bean2",
            "id": 2,
            "lastName": "Bean 2"
        }
    ]
}

そのJSON構造は、シリアル化前と同じサブタイプのオブジェクトを再作成するために使用されます。 逆シリアル化の実装手順は次のとおりです。

BeanContainer deserializedContainer = mapper.readValue(jsonString, BeanContainer.class);
List beanList = deserializedContainer.getBeans();
assertThat(beanList.get(0), instanceOf(FirstBean.class));
assertThat(beanList.get(1), instanceOf(LastBean.class));

9. 結論

このチュートリアルでは、あまり一般的ではないいくつかのJacksonアノテーションについて詳しく説明しました。 これらの例とコードスニペットの実装は、a GitHub projectにあります。