ジャクソンとの相続

ジャクソンとの相続

1. 概要

この記事では、ジャクソンのクラス階層の操作について説明します。

2つの典型的な使用例は、サブタイプメタデータを含めることと、スーパークラスから継承したプロパティを無視することです。 これらの2つのシナリオと、サブタイプの特別な処理が必要ないくつかの状況について説明します。

2. サブタイプ情報の包含

データオブジェクトをシリアル化および逆シリアル化するときに型情報を追加するには、グローバルなデフォルトの型指定とクラスごとの注釈の2つの方法があります。

2.1. グローバルデフォルト入力

次の3つのJavaクラスを使用して、タイプメタデータのグローバルな包含を示します。

Vehicleスーパークラス:

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

    // no-arg constructor, getters and setters
}

Carサブクラス:

public class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

    // no-arg constructor, getters and setters
}

Truckサブクラス:

public class Truck extends Vehicle {
    private double payloadCapacity;

    public Truck(String make, String model, double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

    // no-arg constructor, getters and setters
}

グローバルデフォルトタイピングでは、ObjectMapperオブジェクトでタイプ情報を有効にすることにより、タイプ情報を1回だけ宣言できます。 そのタイプのメタデータは、指定されたすべてのタイプに適用されます。 そのため、特に多数のタイプが関係している場合、このメソッドを使用してタイプメタデータを追加すると非常に便利です。 欠点は、完全修飾Java型名を型識別子として使用するため、非Javaシステムとの相互作用には適さず、いくつかの事前定義された種類の型にのみ適用できることです。

上記のVehicle構造体は、Fleetクラスのインスタンスにデータを入力するために使用されます。

public class Fleet {
    private List vehicles;

    // getters and setters
}

タイプメタデータを埋め込むには、後でデータオブジェクトのシリアル化と逆シリアル化に使用されるObjectMapperオブジェクトで入力機能を有効にする必要があります。

ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

applicabilityパラメータは、タイプ情報を必要とするタイプを決定し、includeAsパラメータは、タイプメタデータを含めるためのメカニズムです。 さらに、enableDefaultTypingメソッドの他の2つのバリアントが提供されています。

  • ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability)includeAsのデフォルト値としてWRAPPER_ARRAYを使用しながら、呼び出し元がapplicabilityを指定できるようにします

  • ObjectMapper.enableDefaultTyping():は、applicabilityのデフォルト値としてOBJECT_AND_NON_CONCRETEを使用し、includeAsのデフォルト値としてWRAPPER_ARRAYを使用します。

仕組みを見てみましょう。 まず、ObjectMapperオブジェクトを作成し、デフォルトの入力を有効にする必要があります。

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

次のステップは、このサブセクションの最初に導入されたデータ構造をインスタンス化し、データを取り込むことです。 それを行うためのコードは、後のサブセクションで再利用されます。 便宜上、再利用するために、vehicle instantiation blockという名前を付けます。

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

これらの移入されたオブジェクトはその後シリアル化されます:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

結果のJSON文字列:

{
    "vehicles":
    [
        "java.util.ArrayList",
        [
            [
                "org.example.jackson.inheritance.Car",
                {
                    "make": "Mercedes-Benz",
                    "model": "S500",
                    "seatingCapacity": 5,
                    "topSpeed": 250.0
                }
            ],

            [
                "org.example.jackson.inheritance.Truck",
                {
                    "make": "Isuzu",
                    "model": "NQR",
                    "payloadCapacity": 7500.0
                }
            ]
        ]
    ]
}

逆シリアル化中に、オブジェクトはJSONデータから復元され、型データが保持されます。

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

再作成されたオブジェクトは、シリアライズ前と同じ具象サブタイプになります。

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

2.2. クラスごとの注釈

クラスごとの注釈は、型情報を含める強力な方法であり、かなりのレベルのカスタマイズが必要な複雑なユースケースに非常に役立ちます。 ただし、これは複雑さを犠牲にしてのみ達成できます。 型情報が両方の方法で構成されている場合、クラスごとの注釈はグローバルなデフォルトの型指定をオーバーライドします。

このメソッドを利用するには、スーパータイプに@JsonTypeInfoと他のいくつかの関連する注釈を付ける必要があります。 このサブセクションでは、前の例のVehicle構造と同様のデータモデルを使用して、クラスごとのアノテーションを示します。 唯一の変更点は、以下に示すように、Vehicleの抽象クラスに注釈を追加することです。

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.PROPERTY,
  property = "type")
@JsonSubTypes({
  @Type(value = Car.class, name = "car"),
  @Type(value = Truck.class, name = "truck")
})
public abstract class Vehicle {
    // fields, constructors, getters and setters
}

データオブジェクトは、前のサブセクションで紹介したvehicle instantiation blockを使用して作成され、シリアル化されます。

String jsonDataString = mapper.writeValueAsString(serializedFleet);

シリアル化により、次のJSON構造が生成されます。

{
    "vehicles":
    [
        {
            "type": "car",
            "make": "Mercedes-Benz",
            "model": "S500",
            "seatingCapacity": 5,
            "topSpeed": 250.0
        },

        {
            "type": "truck",
            "make": "Isuzu",
            "model": "NQR",
            "payloadCapacity": 7500.0
        }
    ]
}

その文字列は、データオブジェクトを再作成するために使用されます。

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

最後に、進行状況全体が検証されます。

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

3. スーパータイプからのプロパティの無視

場合によっては、スーパークラスから継承された一部のプロパティは、シリアル化または逆シリアル化中に無視する必要があります。 これは、注釈、ミックスイン、注釈内省の3つの方法のいずれかで実現できます。

3.1. アノテーション

プロパティを無視するために一般的に使用される2つのJacksonアノテーションがあります。それは、@JsonIgnore@JsonIgnorePropertiesです。 前者は型メンバーに直接適用され、ジャクソンにシリアル化または逆シリアル化するときに対応するプロパティを無視するように伝えます。 後者は、タイプやタイプメンバーを含むあらゆるレベルで使用され、無視されるプロパティをリストします。

@JsonIgnorePropertiesは、外部ライブラリの型など、制御できないスーパータイプから継承されたプロパティを無視できるため、他よりも強力です。 さらに、このアノテーションを使用すると、多くのプロパティを一度に無視できるため、場合によってはより理解しやすいコードになります。

次のクラス構造は、注釈の使用法を示すために使用されます。

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

    // no-arg constructor, getters and setters
}

@JsonIgnoreProperties({ "model", "seatingCapacity" })
public abstract class Car extends Vehicle {
    private int seatingCapacity;

    @JsonIgnore
    private double topSpeed;

    protected Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

    // no-arg constructor, getters and setters
}

public class Sedan extends Car {
    public Sedan(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model, seatingCapacity, topSpeed);
    }

    // no-arg constructor
}

public class Crossover extends Car {
    private double towingCapacity;

    public Crossover(String make, String model, int seatingCapacity,
      double topSpeed, double towingCapacity) {
        super(make, model, seatingCapacity, topSpeed);
        this.towingCapacity = towingCapacity;
    }

    // no-arg constructor, getters and setters
}

ご覧のとおり、@JsonIgnoreはJacksonにCar.topSpeedプロパティを無視するように指示し、@JsonIgnorePropertiesVehicle.modelおよびCar.seatingCapacityプロパティを無視します。

両方の注釈の動作は、次のテストによって検証されます。 まず、ObjectMapperとデータクラスをインスタンス化し、次にそのObjectMapperインスタンスを使用してデータオブジェクトをシリアル化する必要があります。

ObjectMapper mapper = new ObjectMapper();

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataStringには、次のJSON配列が含まれています。

[
    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW",
        "towingCapacity": 6000.0
    }
]

最後に、結果のJSON文字列にさまざまなプロパティ名が存在するかどうかを証明します。

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.2. ミックスイン

ミックスインを使用すると、注釈をクラスに直接適用することなく、動作を適用できます(シリアル化および逆シリアル化するときにプロパティを無視するなど)。 これは、コードを直接変更できないサードパーティクラスを扱う場合に特に便利です。

このサブセクションでは、Carクラスの@JsonIgnoreおよび@JsonIgnorePropertiesアノテーションが削除されていることを除いて、前のサブセクションで紹介したクラス継承チェーンを再利用します。

public abstract class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    // fields, constructors, getters and setters
}

ミックスインの操作を示すために、Vehicle.makeおよびCar.topSpeedプロパティを無視し、テストを使用してすべてが期待どおりに機能することを確認します。

最初のステップは、ミックスインタイプを宣言することです。

private abstract class CarMixIn {
    @JsonIgnore
    public String make;
    @JsonIgnore
    public String topSpeed;
}

次に、ミックスインはObjectMapperオブジェクトを介してデータクラスにバインドされます。

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Car.class, CarMixIn.class);

その後、データオブジェクトをインスタンス化し、文字列にシリアル化します。

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataStringには次のJSONが含まれるようになりました。

[
    {
        "model": "S500",
        "seatingCapacity": 5
    },
    {
        "model": "X6",
        "seatingCapacity": 5,
        "towingCapacity": 6000.0
    }
]

最後に、結果を確認しましょう。

assertThat(jsonDataString, not(containsString("make")));
assertThat(jsonDataString, containsString("model"));
assertThat(jsonDataString, containsString("seatingCapacity"));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.3. 注釈の内省

アノテーションイントロスペクションは、AnnotationIntrospector.hasIgnoreMarker APIを使用して詳細なカスタマイズが可能になるため、スーパータイプのプロパティを無視するための最も強力な方法です。

このサブセクションでは、前のものと同じクラス階層を使用します。 このユースケースでは、Vehicle.modelCrossover.towingCapacity、およびCarクラスで宣言されたすべてのプロパティを無視するようにJacksonに依頼します。 JacksonAnnotationIntrospectorインターフェースを拡張するクラスの宣言から始めましょう。

class IgnoranceIntrospector extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m.getDeclaringClass() == Vehicle.class && m.getName() == "model"
          || m.getDeclaringClass() == Car.class
          || m.getName() == "towingCapacity"
          || super.hasIgnoreMarker(m);
    }
}

イントロスペクターは、メソッドで定義された条件のセットに一致するプロパティを無視します(つまり、他のメソッドのいずれかを介して無視としてマークされたものとして処理します)。

次のステップは、IgnoranceIntrospectorクラスのインスタンスをObjectMapperオブジェクトに登録することです。

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new IgnoranceIntrospector());

次に、セクション3.2と同じ方法でデータオブジェクトを作成してシリアル化します。 新しく生成された文字列の内容は次のとおりです。

[
    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW"
    }
]

最後に、イントロスペクターが意図したとおりに機能したことを確認します。

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, not(containsString("towingCapacity")));

4. サブタイプ処理シナリオ

このセクションでは、サブクラスの処理に関連する2つの興味深いシナリオを扱います。

4.1. サブタイプ間の変換

Jacksonでは、オブジェクトを元のタイプ以外のタイプに変換できます。 実際、この変換は互換性のある型の間で発生する可能性がありますが、値と機能を保護するために同じインターフェイスまたはクラスの2つのサブタイプ間で使用する場合に最も役立ちます。

タイプから別のタイプへの変換を示すために、セクション2から取得したVehicle階層を再利用し、CarおよびTruckのプロパティに@JsonIgnoreアノテーションを追加します。 )非互換性を回避するため。

public class Car extends Vehicle {
    @JsonIgnore
    private int seatingCapacity;

    @JsonIgnore
    private double topSpeed;

    // constructors, getters and setters
}

public class Truck extends Vehicle {
    @JsonIgnore
    private double payloadCapacity;

    // constructors, getters and setters
}

次のコードは、変換が成功し、新しいオブジェクトが古いオブジェクトのデータ値を保持することを確認します。

ObjectMapper mapper = new ObjectMapper();

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = mapper.convertValue(car, Truck.class);

assertEquals("Mercedes-Benz", truck.getMake());
assertEquals("S500", truck.getModel());

4.2. 引数なしのコンストラクタを使用しない逆シリアル化

デフォルトでは、Jacksonは引数なしのコンストラクターを使用してデータオブジェクトを再作成します。 これは、クラスにデフォルト以外のコンストラクターがあり、ユーザーがジャクソンの要件を満たすためだけに引数なしのコンストラクターを作成する必要がある場合など、場合によっては不便です。 引数なしのコンストラクターをクラスに追加する必要があるクラス階層や、継承チェーンの上位クラスでは、さらに面倒です。 このような場合、creator methodsが役に立ちます。

このセクションでは、コンストラクターにいくつかの変更を加えた、セクション2のオブジェクト構造に類似したオブジェクト構造を使用します。 具体的には、引数のないコンストラクターはすべて削除され、具象サブタイプのコンストラクターには@JsonCreator@JsonPropertyの注釈が付けられ、作成者メソッドになります。

public class Car extends Vehicle {

    @JsonCreator
    public Car(
      @JsonProperty("make") String make,
      @JsonProperty("model") String model,
      @JsonProperty("seating") int seatingCapacity,
      @JsonProperty("topSpeed") double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
            this.topSpeed = topSpeed;
    }

    // fields, getters and setters
}

public class Truck extends Vehicle {

    @JsonCreator
    public Truck(
      @JsonProperty("make") String make,
      @JsonProperty("model") String model,
      @JsonProperty("payload") double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

    // fields, getters and setters
}

テストは、ジャクソンが引数なしのコンストラクタを持たないオブジェクトを処理できることを確認します。

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

String jsonDataString = mapper.writeValueAsString(serializedFleet);
mapper.readValue(jsonDataString, Fleet.class);

5. 結論

このチュートリアルでは、ポリモーフィズムとスーパータイププロパティの無知に焦点を当てて、型継承に対するジャクソンのサポートを示すいくつかの興味深いユースケースについて説明しました。

これらすべての例とコードスニペットの実装は、a GitHub projectにあります。