CodeGym /コース /JAVA 25 SELF /XML シリアライゼーションの調整: カスタムアダプタ

XML シリアライゼーションの調整: カスタムアダプタ

JAVA 25 SELF
レベル 47 , レッスン 4
使用可能

1. アダプタの利用(@XmlJavaTypeAdapter

JAXB はオートマ車のトランスミッションのようなものです。標準的なケースなら完璧に動作しますが、少しでも変わったものが出てくると手動の介入が必要になります。たとえば、クラスに LocalDateBigDecimal 型のフィールドを追加したとします。JAXB は戸惑います — それらを XML に変換したり元に戻したりする方法を知らないからです。あるいは、日付を長い文字列 2024-06-01T00:00:00 ではなく、馴染みのある形式 01.06.2024 で出力したいかもしれません。属性に入れた方が自然なオブジェクトがあったり、特別な表現が必要な入れ子のコレクションがあることもあるでしょう。

こうした状況はアダプタで解決できます。アダプタを使えば、複雑なフィールドをどのようにシリアライズ/デシリアライズするか、どんなフォーマットにするか、不要なデータを省くかなどを JAXB に指示できます。オートマが対応できない場面で手動の「ギア操作」を行い、柔軟性を得るイメージです。

アダプタとは?

アダプタとは、JAXB に対して「この型に出会ったらこうシリアライズし、こうデシリアライズしてね」と伝える特別なクラスです。Java ではアダプタは抽象クラス javax.xml.bind.annotation.adapters.XmlAdapter<ValueType, BoundType> を実装します。ここで:

  • ValueType — XML での表現方法(通常は String、ときには IntegerLong、あるいは別のオブジェクト)。
  • BoundType — 実際の Java クラスの型(例: LocalDate)。

例: LocalDate 型フィールドのシリアライズ

import java.time.LocalDate;
import javax.xml.bind.annotation.*;

@XmlRootElement
public class Person {
    private String name;
    private LocalDate birthDate; // ここが問題!

    public Person() {} // JAXB は引数なしの public コンストラクタを要求します

    public Person(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }

    @XmlElement
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    @XmlElement
    public LocalDate getBirthDate() { return birthDate; }
    public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
}

このオブジェクトをシリアライズしようとすると、JAXB は例外を投げます:

javax.xml.bind.JAXBException: class java.time.LocalDate nor any of its super class is known to this context.

ステップ1: アダプタを作成する

import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

// LocalDate <-> String の変換用アダプタ
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd.MM.yyyy");

    @Override
    public LocalDate unmarshal(String v) throws Exception {
        return (v == null || v.isEmpty()) ? null : LocalDate.parse(v, FORMATTER);
    }

    @Override
    public String marshal(LocalDate v) throws Exception {
        return (v == null) ? null : v.format(FORMATTER);
    }
}
  • marshal — Java オブジェクト(LocalDate)を XML の文字列へ変換します。
  • unmarshal — XML の文字列から Java オブジェクトへ戻します。

ステップ2: フィールドまたはゲッターにアノテーションを付ける

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }

アノテーションはフィールド自体に付けても構いません:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;

ステップ3: 結果を確認する

シリアライズすると、オブジェクトは次のようになります:

<Person>
    <name>Ivan</name>
    <birthDate>01.06.2024</birthDate>
</Person>

逆に、XML から読み込むと、文字列 "01.06.2024"LocalDate オブジェクトに変換されます。

2. アダプタをフィールド、ゲッター、またはクラス全体に適用する

アダプタは用途に応じてさまざまに適用できます。

個々のフィールドまたはゲッターに: 最も一般的なケースです。

@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate birthDate;

クラス全体に: ある型を常にアダプタ経由でシリアライズしたい場合は、型そのものに注釈を付けられます:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public class LocalDate { ... }

通常は標準クラスではなく自分のクラスに対して行います(LocalDate クラス自体は変更できません)。

コレクションに: 例えば、List<LocalDate> を日付文字列のリストに変換するアダプタでシリアライズできます。

3. 要素名・属性名のカスタマイズ

XML 構造に厳密な要件がある場合があります。例えば、フィールド名を <birthDate> ではなく <birth_date> にしたい、あるいは生年月日を要素ではなく属性として表したい、といったケースです。

要素名の変更

@XmlElement(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }

XML は次のようになります:

<birth_date>01.06.2024</birth_date>

属性としてシリアライズ

@XmlAttribute(name = "birth_date")
public LocalDate getBirthDate() { return birthDate; }

XML:

<Person birth_date="01.06.2024">
    <name>Ivan</name>
</Person>

アダプタとの併用

@XmlAttribute(name = "birth_date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthDate() { return birthDate; }

4. 実用的なケース

フィールドのスキップ(@XmlTransient

あるフィールドを XML に一切出力したくないことがあります(内部 ID、パスワード、一時データなど)。

@XmlTransient
private String internalCode;

このようなフィールドはシリアライズ/デシリアライズの対象外になります。

数値の書式化

金額のフィールドがあるとします:

private BigDecimal balance;

JAXBBigDecimal を希望するフォーマット(例えば小数点以下 2 桁、カンマ区切りなど)でシリアライズできません。そこでアダプタを書きます:

import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.math.BigDecimal;

public class BigDecimalAdapter extends XmlAdapter<String, BigDecimal> {
    @Override
    public BigDecimal unmarshal(String v) throws Exception {
        return (v == null || v.isEmpty()) ? null : new BigDecimal(v.replace(",", "."));
    }

    @Override
    public String marshal(BigDecimal v) throws Exception {
        return (v == null) ? null : String.format("%.2f", v);
    }
}

使い方:

@XmlJavaTypeAdapter(BigDecimalAdapter.class)
private BigDecimal balance;

入れ子の構造

入れ子のオブジェクトがある場合、例えば:

public class Address {
    private String city;
    private String street;
    // ...
}

JAXB は入れ子のオブジェクトをデフォルトで要素としてシリアライズします。しかし、例えば city は属性に、street は要素にしたい場合は、アノテーションを使います:

public class Address {
    @XmlAttribute
    private String city;
    @XmlElement
    private String street;
}

5. 例: アダプタを使ったシリアライゼーションの完全設定

アプリケーションを拡張しましょう。生年月日と残高を持つクラス Person を用意します。

import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.math.BigDecimal;
import java.time.LocalDate;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {
    @XmlElement
    private String name;

    @XmlAttribute(name = "birth_date")
    @XmlJavaTypeAdapter(LocalDateAdapter.class)
    private LocalDate birthDate;

    @XmlElement
    @XmlJavaTypeAdapter(BigDecimalAdapter.class)
    private BigDecimal balance;

    @XmlTransient
    private String password;

    public Person() {}

    public Person(String name, LocalDate birthDate, BigDecimal balance, String password) {
        this.name = name;
        this.birthDate = birthDate;
        this.balance = balance;
        this.password = password;
    }

    // getter と setter など...
}

得られる結果:

  • 名前は要素 <name> としてシリアライズされます。
  • 生年月日は属性 <Person birth_date="01.06.2024"> としてシリアライズされます。
  • 残高は要素 <balance>1234.56</balance> としてシリアライズされます。
  • パスワードは XML に一切含まれません。

XML ファイル:

<Person birth_date="01.06.2024">
    <name>Ivan</name>
    <balance>1234.56</balance>
</Person>

6. コレクションと入れ子オブジェクトの処理

JAXB は、コレクションが適切に注釈されていれば扱えます。例えば、人が複数の住所を持つ場合:

@XmlElementWrapper(name = "addresses")
@XmlElement(name = "address")
private List<Address> addresses;

XML は次のようになります:

<addresses>
    <address city="Berlin">
        <street>Aleksandrplatts, 1</street>
    </address>
    <address city="Limassol">
        <street>Aneksartisias, 10</street>
    </address>
</addresses>

コレクション内の型が非標準(例えば List<LocalDate>)であれば、コレクション要素にアダプタを適用できます:

@XmlElementWrapper(name = "dates")
@XmlElement(name = "date")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private List<LocalDate> importantDates;

7. 例: アダプタを使ったシリアライズとデシリアライズ

シリアライズ

Person person = new Person(
    "Ivan",
    LocalDate.of(1990, 6, 1),
    new BigDecimal("1234.56"),
    "secretPassword"
);

JAXBContext context = JAXBContext.newInstance(Person.class);
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

marshaller.marshal(person, System.out); // XML をコンソールに出力します

デシリアライズ

String xml = """
    <Person birth_date="01.06.1990">
        <name>Ivan</name>
        <balance>1234.56</balance>
    </Person>
    """;

JAXBContext context = JAXBContext.newInstance(Person.class);
Unmarshaller unmarshaller = context.createUnmarshaller();

Person person = (Person) unmarshaller.unmarshal(new StringReader(xml));
System.out.println(person.getName() + " " + person.getBirthDate() + " " + person.getBalance());

8. シリアライゼーションとアダプタ設定でよくあるエラー

エラー1: 引数なしの public コンストラクタがない。 これがないと、JAXB はデシリアライズ時にオブジェクトを生成できず、例外を投げます。

エラー2: アダプタの適用箇所が誤っている。 @XmlJavaTypeAdapter を別のフィールドに付けてしまったり、ゲッター/セッターを忘れると、JAXB は期待する型を正しくシリアライズできません。

エラー3: デシリアライズ時のフォーマット不一致。 XML の日付がアダプタでサポートしていない形式(例えば "2024-06-01" で、期待は "01.06.2024")だと、unmarshal メソッドは例外を投げます。

エラー4: JAXB がサポートしない型をアダプタなしでシリアライズしようとする。 典型例は LocalDateBigDecimalMap、独自の複合型などです。

エラー5: アノテーションを付けずに入れ子のコレクションを扱う。 @XmlElementWrapper がないと、コレクションは期待通りにシリアライズされなかったり、JAXB が XML を正しく読み戻せないことがあります。

エラー6: アダプタをコレクションそのものに適用してしまい、要素に適用していない。 リストの要素をアダプタ経由でシリアライズしたい場合は、アノテーションは要素に付けてください(例えば、要素のフィールドに @XmlJavaTypeAdapter を付ける、あるいは上の例のようにリストの要素型に対して指定します)。

1
タスク
JAVA 25 SELF, レベル 47, レッスン 4
ロック未解除
イベントの日付を属性として記録する 📅
イベントの日付を属性として記録する 📅
1
タスク
JAVA 25 SELF, レベル 47, レッスン 4
ロック未解除
カレンダーの祝日リストの管理 🎉
カレンダーの祝日リストの管理 🎉
1
アンケート/クイズ
XMLのシリアライズ、レベル 47、レッスン 4
使用不可
XMLのシリアライズ
XMLのシリアライゼーション
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION