1. 概要

この記事では、ジャクソンのクラス階層での作業について見ていきます。

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

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

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

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

次の3つのJavaクラスは、タイプメタデータのグローバルな包含を説明するために使用されます。

車両スーパークラス:

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
}

トラックサブクラス:

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<Vehicle> vehicles;
    
    // getters and setters
}

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

ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv, 
  ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

パラメーターPolymorphicTypeValidatorは、逆シリアル化する実際のサブタイプが指定された基準に従って有効であることを確認するために使用されます。 さらに、 applyability パラメーターは、タイプ情報を必要とするタイプを決定し、 includeAs パラメーターは、タイプメタデータを含めるためのメカニズムです。 さらに、activateDefaultTypingメソッドの他の2つのバリアントが提供されています。

  • ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv、ObjectMapper.DefaultTyping applicability):呼び出し元が WRAPPER_ARRAY を使用しながら、validatorおよびapplicabilityを指定できるようにします。 includeAsのデフォルト値として
  • ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv):は、 OBJECT_AND_NON_CONCRETE適用性および[ X224X] WRAPPER_ARRAY includeAsのデフォルト値として

それがどのように機能するか見てみましょう。 まず、バリデーターを作成する必要があります。

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
  .allowIfSubType("com.baeldung.jackson.inheritance")
  .allowIfSubType("java.util.ArrayList")
  .build();

次に、 ObjectMapper オブジェクトを作成し、上記のバリデーターを使用してデフォルトの入力をアクティブにします。

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);

次のステップは、このサブセクションの冒頭で紹介したデータ構造をインスタンス化して入力することです。 それを行うためのコードは、後のサブセクションで再利用されます。 利便性と再利用のために、車両インスタンス化ブロックと名付けます。

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

List<Vehicle> 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",
        [
            [
                "com.baeldung.jackson.inheritance.Car",
                {
                    "make": "Mercedes-Benz",
                    "model": "S500",
                    "seatingCapacity": 5,
                    "topSpeed": 250.0
                }
            ],

            [
                "com.baeldung.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
}

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

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です。 前者はタイプメンバーに直接適用され、シリアル化または逆シリアル化するときに対応するプロパティを無視するようにJacksonに指示します。 後者は、タイプやタイプメンバーを含むすべてのレベルで使用され、無視する必要のあるプロパティを一覧表示します。

@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と[ X169X]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<Vehicle> 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<Vehicle> 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を使用して詳細なカスタマイズができるため、スーパータイプのプロパティを無視するための最も強力な方法です。

このサブセクションでは、前のサブセクションと同じクラス階層を使用します。 このユースケースでは、Jacksonに Vehicle.model Crossover.towingCapacity 、およびCarクラスで宣言されたすべてのプロパティを無視するように依頼します。 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);
    }
}

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

次のステップは、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. サブタイプ間の変換

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

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

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は引数なしのコンストラクターを使用してデータオブジェクトを再作成します。 これは、クラスにデフォルト以外のコンストラクターがあり、ユーザーがジャクソンの要件を満たすためだけに引数なしのコンストラクターを作成する必要がある場合など、場合によっては不便です。 クラス階層では、引数なしのコンストラクターをクラスに追加する必要があり、継承チェーンの上位にあるすべてのコンストラクターを追加する必要がある場合は、さらに厄介です。 このような場合、作成者メソッドが役に立ちます。

このセクションでは、セクション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
}

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

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<Vehicle> 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. 結論

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

これらすべての例とコードスニペットの実装は、GitHubプロジェクトにあります。