JSON-B とは
- Java EE 8 で追加された仕様で、 JSON と Java オブジェクトの相互変換を行う API を定義している
- Jakarta EE での正式名称は Jakarta JSON Binding
- JAXB の JSON 版みたいなの
- B は Binding の B
- 読み方は、たぶん「じぇいそんびー」
環境
>gradle --version
------------------------------------------------------------
Gradle 8.8
------------------------------------------------------------
Build time: 2024-05-31 21:46:56 UTC
Revision: 4bd1b3d3fc3f31db5a26eecb416a165b8cc36082
Kotlin: 1.9.22
Groovy: 3.0.21
Ant: Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM: 21.0.3 (Eclipse Adoptium 21.0.3+9-LTS)
OS: Windows 10 10.0 amd64
Hello World
実装
plugins {
id "java"
}
compileJava.options.encoding = "UTF-8"
sourceCompatibility = 21
targetCompatibility = 21
repositories {
mavenCentral()
}
dependencies {
implementation "org.eclipse:yasson:3.0.4"
}
package sandbox.jsonb;
import java.util.List;
public class MyBean {
public int number;
public String string;
public boolean bool;
public List<String> stringList;
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import java.util.List;
public class JsonbMain {
public static void main(String[] args) throws Exception {
MyBean bean = new MyBean();
bean.number = 10;
bean.bool = true;
bean.string = "Hello JSON-B!!";
bean.stringList = List.of("a", "b", "c");
try (Jsonb jsonb = JsonbBuilder.create()) {
String json = jsonb.toJson(bean);
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println(deserialized);
}
}
}
実行結果
{"bool":true,"number":10,"string":"Hello JSON-B!!","stringList":["a","b","c"]}
MyBean{number=10, string='Hello JSON-B!!', bool=true, stringList=[a, b, c]}
説明
dependencies {
implementation "org.eclipse:yasson:3.0.4"
}
- Java SE 環境で使いたい場合は JSON-B を実装したライブラリを追加する必要がある
- ここでは互換実装である Eclipse Yasson を指定している
Jsonb jsonb = JsonbBuilder.create();
String json = jsonb.toJson(bean);
...
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
- JSON-B は
Jsonbを使ってシリアライズ・デシリアライズを行う-
toJsonで、 Java オブジェクトを JSON にシリアライズする-
OutputStreamやWriterに書き出すメソッドも用意されている
-
-
fromJsonで、 JSON から Java オブジェクトにデシリアライズする-
InputStreamやReaderから読み取るメソッドも用意されている - デシリアライズ対象のクラスにはデフォルトコンストラクタが存在しなければならない
-
- 文字コードが指定されていない場合、デフォルトは UTF-8 で処理される
-
-
JsonbのインスタンスはJsonbBuilderのcreateメソッドで取得できる -
Jsonbインスタンスはスレッドセーフなので、1つだけ生成して使いまわすことができる
シリアライズ・デシリアライズ対象の仕様
シリアライズ対象の仕様
実装
package sandbox.jsonb;
public class MyBean {
public static String staticMember = "staticMember";
private String privateMember = "privateMember";
protected String protectedMember = "protectedMember";
String packagePrivateMember = "packagePrivateMember";
public String publicMember = "publicMember";
public String publicMember2 = "publicMember2";
public String getPublicMember2() {
return "getPublicMember2";
}
public String publicMember3() {
return "publicMember3";
}
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
public class JsonbMain {
public static void main(String[] args) throws Exception {
try (Jsonb jsonb = JsonbBuilder.create()) {
String json = jsonb.toJson(new MyBean());
System.out.println(json);
}
}
}
実行結果
{"publicMember":"publicMember","publicMember2":"getPublicMember2"}
説明
- Getter がある場合は Getter 優先で値が取得される
- Getter がなく、
publicなフィールドのみある場合は、フィールドから直接値が取得される -
publicでないフィールドはシリアライズの対象にならない -
publicであっても、staticなフィールドはシリアライズの対象にならない
デシリアライズ対象の仕様
実装
package sandbox.jsonb;
public class MyBean {
public static String staticMember;
private String privateMember;
protected String protectedMember;
String packagePrivateMember;
public String publicMember;
public String publicMember2;
private String privateMember2;
public void setPublicMember2(String publicMember2) {
this.publicMember2 = "set(" + publicMember2 + ")";
}
public void privateMember2(String privateMember2) {
this.privateMember2 = privateMember2;
}
@Override
public String toString() {
return "MyBean {\n" +
" staticMember='" + staticMember + '\'' +
",\n privateMember='" + privateMember + '\'' +
",\n protectedMember='" + protectedMember + '\'' +
",\n packagePrivateMember='" + packagePrivateMember + '\'' +
",\n publicMember='" + publicMember + '\'' +
",\n publicMember2='" + publicMember2 + '\'' +
",\n privateMember2='" + privateMember2 + '\'' +
"\n}";
}
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
public class JsonbMain {
public static void main(String[] args) throws Exception {
try (Jsonb jsonb = JsonbBuilder.create()) {
MyBean bean = jsonb.fromJson("""
{
"staticMember": "staticMember",
"privateMember": "privateMember",
"protectedMember": "protectedMember",
"packagePrivateMember": "packagePrivateMember",
"publicMember": "publicMember",
"publicMember2": "publicMember2",
"privateMember2": "privateMember2"
}
""", MyBean.class);
System.out.println(bean);
}
}
}
実行結果
MyBean {
staticMember='null',
privateMember='null',
protectedMember='null',
packagePrivateMember='null',
publicMember='publicMember',
publicMember2='set(publicMember2)',
privateMember2='null'
}
説明
- Setter がある場合は Setter 優先で値が設定される
- Setter がなく、
publicなフィールドのみがある場合は、フィールドに直接値が設定される -
publicでないフィールドはデシリアライズの対象にならない -
publicであっても、staticなフィールドはデシリアライズの対象にならない - マッピング先のフィールドがない場合、エラーにはならず無視される
インデントを入れる
実装
package sandbox.jsonb;
public class MyBean {
public String string = "string";
public int number = 123;
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println(json);
}
}
}
実行結果
{
"number": 123,
"string": "string"
}
説明
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
- JSON にインデントを入れて人間が見やすい形にフォーマットすることができる
-
JsonbConfigを用意して、withFormattingにtrueを設定する - この
JsonbConfigをJsonbBuilderのcreateに渡すことで、フォーマット時にインデントが入るようになる
入れ子のオブジェクトのマッピング
実装
package sandbox.jsonb;
public class Hoge {
public Fuga fuga = new Fuga();
public String text = "Hoge";
// toString省略
}
package sandbox.jsonb;
public class Fuga {
public String message = "Fuga";
public int number = 12;
// toString省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
Hoge bean = new Hoge();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
Hoge deserialized = jsonb.fromJson(json, Hoge.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
実行結果
== Serialize ==
{
"fuga": {
"message": "Fuga",
"number": 12
},
"text": "Hoge"
}
== Deserialize ==
Hoge {
fuga = Fuga {
message = "Fuga"
number = 12
}
text = "Hoge"
}
説明
- オブジェクトが入れ子になっていても問題なくマッピングできる
エンコーディングを指定する
package sandbox.jsonb;
public class MyBean {
public String message = "おはよう世界";
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
config.withEncoding("Shift_JIS");
Path jsonFile = Path.of("test.json");
try (
Jsonb jsonb = JsonbBuilder.create(config);
OutputStream out = Files.newOutputStream(jsonFile);
InputStream in = Files.newInputStream(jsonFile);
) {
MyBean bean = new MyBean();
jsonb.toJson(bean, out);
MyBean deserialized = jsonb.fromJson(in, MyBean.class);
System.out.println(deserialized);
}
}
}
実行結果
{
"message": "おはよう世界"
}
MyBean {
message = "おはよう世界"
}
説明
config.withEncoding("Shift_JIS");
-
JsonbConfigのwithEncodingでデフォルトの文字コードを指定できる - 未指定の場合は UTF-8 になる
- これは RFC 7159 (The JavaScript Object Notation (JSON) Data Interchange Format) に準拠しているため
各 Java 型とのマッピング仕様
文字列型
package sandbox.jsonb;
public class MyBean {
public String stringValue = "String";
public char charValue = 'c';
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
== Serialize ==
{
"charValue": "c",
"stringValue": "String"
}
== Deserialize ==
MyBean {
stringValue = "String"
charValue = c
}
-
String,char,Characterは JSON の文字列とマッピングされる- 特に指定がなければ、文字コードは UTF-8 で処理される
数値型
package sandbox.jsonb;
public class MyBean {
public byte byteValue = 1;
public short shortValue = 2;
public int intValue = 3;
public long longValue = 4;
public float floatValue = 5.1f;
public double doubleValue = 6.2d;
// toString 省略
}
== Serialize ==
{
"byteValue": 1,
"doubleValue": 6.2,
"floatValue": 5.1,
"intValue": 3,
"longValue": 4,
"shortValue": 2
}
== Deserialize ==
MyBean {
byteValue = 1
shortValue = 2
intValue = 3
longValue = 4
floatValue = 5.1
doubleValue = 6.2
}
-
byte,short,int,long,float,doubleおよびそのラッパー型は、 JSON の数値とマッピングされる - シリアライズ時は、ラッパー型の
toStringメソッドの仕様に従って変換される - デシリアライズ時は、
parse$Typeメソッドの仕様に従って変換される(byteの場合はparseByte)
真偽値
package sandbox.jsonb;
public class MyBean {
public boolean booleanValue = true;
// toString 省略
}
== Serialize ==
{
"booleanValue": true
}
== Deserialize ==
MyBean {
booleanValue = true
}
-
booleanおよびラッパー型は、 JSON の真偽値にマッピングされる - シリアライズ時は、ラッパー型の
toStringの仕様に従って変換される - デシリアライズ時は
parseBooleanの仕様に従って変換される
BigInteger, BigDecimal
package sandbox.jsonb;
import java.math.BigDecimal;
import java.math.BigInteger;
public class MyBean {
public BigInteger bigInteger = new BigInteger("12345");
public BigDecimal bigDecimal = new BigDecimal("123.45");
// toString 省略
}
== Serialize ==
{
"bigDecimal": 123.45,
"bigInteger": 12345
}
== Deserialize ==
MyBean {
bigInteger = 12345
bigDecimal = 123.45
}
-
BigInteger,BigDecimalも、 JSON の数値としてマッピングされる - シリアライズ時は、各クラスの
toStringの仕様に従って変換される - デシリアライズ時は、各クラスの
Stringを受け取るコンストラクタの仕様に従って変換される
URL, URI
package sandbox.jsonb;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
public class MyBean {
public URI uri = URI.create("http://localhost:8080/hello");
public URL url;
public MyBean() throws MalformedURLException {
url = uri.toURL();
}
// toString 省略
}
== Serialize ==
{
"uri": "http://localhost:8080/hello",
"url": "http://localhost:8080/hello"
}
== Deserialize ==
MyBean {
uri = http://localhost:8080/hello
url = http://localhost:8080/hello
}
-
URL,URIは、 JSON の文字列とマッピングされる - シリアライズ時は、各クラスの
toStringの仕様に従って変換される - デシリアライズ時は、各クラスの
Stringを受け取るコンストラクタの仕様に従って変換される
Optional
package sandbox.jsonb;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
public class MyBean {
public Optional<String> optional = Optional.of("optional");
public OptionalInt optionalInt = OptionalInt.of(10);
public OptionalLong optionalLong = OptionalLong.of(20);
public OptionalDouble optionalDouble = OptionalDouble.of(30.1);
public Optional<String> empty = Optional.empty();
// toString 省略
}
== Serialize ==
{
"optional": "optional",
"optionalDouble": 30.1,
"optionalInt": 10,
"optionalLong": 20
}
== Deserialize ==
MyBean {
optional = Optional[optional]
optionalInt = OptionalInt[10]
optionalLong = OptionalLong[20]
optionalDouble = OptionalDouble[30.1]
empty = Optional.empty
}
-
Optional,OptionalInt,OptionalLong,OptionalDoubleは、その中身の値に対応する JSON の型にマッピングされる - シリアライズ・デシリアライズ時は、中身の値の型に従った変換が行われる
- 値が空の場合は
null扱いになる
- 値が空の場合は
-
nullをデシリアライズするときは空のOptionalとしてデシリアライズされる
日付
Date, Calendar
package sandbox.jsonb;
import java.util.Calendar;
import java.util.Date;
public class MyBean {
public Date date = new Date();
public Calendar calendar = Calendar.getInstance();
// toString 省略
}
== Serialize ==
{
"calendar": "2024-09-23T16:22:30.979+09:00[Asia/Tokyo]",
"date": "2024-09-23T07:22:30.972Z[UTC]"
}
== Deserialize ==
MyBean {
date = Mon Sep 23 16:22:30 JST 2024
calendar = java.util.GregorianCalendar[省略]
}
-
java.util.Date,Calendarは、 JSON の文字列とマッピングされる - シリアライズ時は DateTimeFormatter の ISO_DATE または ISO_DATE_TIME に従ってフォーマットされる
- 時刻の有無によってフォーマットは変化する
TimeZone
package sandbox.jsonb;
import java.util.TimeZone;
public class MyBean {
public TimeZone timeZone = TimeZone.getDefault();
// toString 省略
}
== Serialize ==
{
"timeZone": "Asia/Tokyo"
}
== Deserialize ==
MyBean {
timeZone = java.util.SimpleTimeZone[省略]
}
-
TimeZoneは、 JSON の文字列とマッピングされる - シリアライズ時、仕様では TimeZone の Javadoc で説明されている NormalizedCustomID に従って変換されると書かれているが、実際に動かすとタイムゾーンIDでも変換された(NormalizedCustomID でも変換可能)
The serialization format of java.util.TimeZone and SimpleTimeZone is NormalizedCustomID as specified in java.util.TimeZone.
(訳)
java.util.TimeZone および SimpleTimeZone のシリアル化形式は、java.util.TimeZone で指定されている NormalizedCustomID です。https://jakarta.ee/specifications/jsonb/3.0/jakarta-jsonb-spec-3.0#java-util-timezone-simpletimezone
Date and Time API の日付・時刻等の型
package sandbox.jsonb;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
public class MyBean {
public Instant instant = Instant.now();
public LocalDate localDate = LocalDate.now();
public LocalTime localTime = LocalTime.now();
public LocalDateTime localDateTime = LocalDateTime.now();
public ZonedDateTime zonedDateTime = ZonedDateTime.now();
public OffsetDateTime offsetDateTime = OffsetDateTime.now();
public OffsetTime offsetTime = OffsetTime.now();
// toString 省略
}
== Serialize ==
{
"instant": "2024-09-23T07:40:10.805080400Z",
"localDate": "2024-09-23",
"localDateTime": "2024-09-23T16:40:10.812083",
"localTime": "16:40:10.812083",
"offsetDateTime": "2024-09-23T16:40:10.8130825+09:00",
"offsetTime": "16:40:10.8130825+09:00",
"zonedDateTime": "2024-09-23T16:40:10.8130825+09:00[Asia/Tokyo]"
}
== Deserialize ==
MyBean {
instant = 2024-09-23T07:40:10.805080400Z
localDate = 2024-09-23
localTime = 16:40:10.812083
localDateTime = 2024-09-23T16:40:10.812083
zonedDateTime = 2024-09-23T16:40:10.813082500+09:00[Asia/Tokyo]
offsetDateTime = 2024-09-23T16:40:10.813082500+09:00
offsetTime = 16:40:10.813082500+09:00
}
-
java.time.*以下の日時系のオブジェクトは、 JSON の文字列とマッピングされる - シリアライズ時、それぞれの型は
DateTimeFormatterで定義されている以下のフォーマットで変換される
| 型 | フォーマット |
|---|---|
java.time.Instant |
ISO_INSTANT |
java.time.LocalDate |
ISO_LOCAL_DATE |
java.time.LocalTime |
ISO_LOCAL_TIME |
java.time.LocalDateTime |
ISO_LOCAL_DATE_TIME |
java.time.ZonedDateTime |
ISO_ZONED_DATE_TIME |
java.time.OffsetDateTime |
ISO_OFFSET_DATE_TIME |
java.time.OffsetTime |
ISO_OFFSET_TIME |
ZoneId, ZoneOffset
package sandbox.jsonb;
import java.time.ZoneId;
import java.time.ZoneOffset;
public class MyBean {
public ZoneId zoneId = ZoneId.systemDefault();
public ZoneOffset zoneOffset= ZoneOffset.ofHours(9);
// toString 省略
}
== Serialize ==
{
"zoneId": "Asia/Tokyo",
"zoneOffset": "+09:00"
}
== Deserialize ==
MyBean {
zoneId = Asia/Tokyo
zoneOffset = +09:00
}
-
ZoneId,ZoneOffsetは、 JSON の文字列にマッピングされる - シリアライズ時は正規化されたゾーンIDに変換される
Duration, Period
package sandbox.jsonb;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class MyBean {
public Duration duration = Duration.of(4567, ChronoUnit.SECONDS);
public Period period = Period.of(10, 5, 3);
// toString 省略
}
== Serialize ==
{
"duration": "PT1H16M7S",
"period": "P10Y5M3D"
}
== Deserialize ==
MyBean {
duration = PT1H16M7S
period = P10Y5M3D
}
-
Duration,Periodは、 JSON の文字列とマッピングされる - シリアライズ時、 ISO 8601 の期間のフォーマットで変換される
- ISO 8601 の期間のフォーマットは以下のような書式になっている
P[n]Y[n]M[n]DT[n]H[n]M[n]S
-
Pは「Period」を表す文字で、常に先頭につけられる -
Tは時分秒の前につける識別子 - これ以外は、それぞれ時間の量(
n)と単位(Y,M, ...)の組み合わせで表現されている
| 単位 | 意味 |
|---|---|
Y |
年 |
M |
月 |
D |
日 |
H |
時 |
M |
分 |
S |
秒 |
型を指定しない場合のマッピング
package sandbox.jsonb;
public class MyBean {
public Object object;
public Object array;
public Object string;
public Object number;
public Object bool;
public Object nullValue;
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
public class JsonbMain {
public static void main(String[] args) throws Exception {
try (Jsonb jsonb = JsonbBuilder.create()) {
MyBean bean = jsonb.fromJson("""
{
"object": {
"hoge": "HOGE",
"fuga": "FUGA",
"piyo": "PIYO"
},
"array": [1, 2, 3],
"string": "STRING",
"number": 123,
"bool": true,
"nullValue": null
}
""", MyBean.class);
print("object", bean.object);
print("array", bean.array);
print("string", bean.string);
print("number", bean.number);
print("bool", bean.bool);
print("nullValue", bean.nullValue);
}
}
private static void print(String name, Object value) {
System.out.printf("%s: type=%s, value=%s%n",
name,
value == null ? "" : value.getClass().getName(),
value);
}
}
object: type=java.util.HashMap, value={hoge=HOGE, fuga=FUGA, piyo=PIYO}
array: type=java.util.ArrayList, value=[1, 2, 3]
string: type=java.lang.String, value=STRING
number: type=java.math.BigDecimal, value=123
bool: type=java.lang.Boolean, value=true
nullValue: type=, value=null
- Java 側の型を指定しない場合(
Objectで受けた場合)、各 JSON の値は以下のようにマッピングされる
| JSONでの型 | Javaの型 |
|---|---|
object |
java.util.Map<String, Object> |
array |
java.util.List<Object> |
string |
java.lang.String |
number |
java.math.BigDecimal |
boolean |
java.lang.Boolean |
null |
null |
-
objectについては、仕様では「予測可能な反復順序 (predictable iteration order)」と書かれており、一見すると順序が保存されるLinkedHashMapあたりが使われそうに思えたが、実際にはHashMapになった(バグ?)
JSON object values are deserialized into an implementation of java.util.Map with a predictable iteration order.
(訳)
JSON オブジェクト値は、予測可能な反復順序で java.util.Map の実装に逆シリアル化されます。https://jakarta.ee/specifications/jsonb/3.0/jakarta-jsonb-spec-3.0#untyped-mapping
コレクション
package sandbox.jsonb;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
public class MyBean {
public List<Integer> list = List.of(1, 2, 3);
public Map<String, String> map
= Map.of("hoge", "HOGE", "fuga", "FUGA", "piyo", "PIYO");
public Set<Integer> set = Set.of(9, 8, 7);
public Queue<String> queue = new LinkedList<>(
List.of("a", "b", "c")
);
// toString 省略
}
== Serialize ==
{
"list": [
1,
2,
3
],
"map": {
"piyo": "PIYO",
"fuga": "FUGA",
"hoge": "HOGE"
},
"queue": [
"a",
"b",
"c"
],
"set": [
9,
8,
7
]
}
== Deserialize ==
MyBean {
list = [1, 2, 3]
map = {hoge=HOGE, fuga=FUGA, piyo=PIYO}
set = [7, 8, 9]
queue = [a, b, c]
}
-
ListやMapなどのコレクションは、 JSON のオブジェクトまたはリストにマッピングされる
配列
package sandbox.jsonb;
public class MyBean {
public String[] strings = {"hoge", "fuga", "piyo"};
public int[] ints = {1, 2, 3};
// toString 省略
}
== Serialize ==
{
"ints": [
1,
2,
3
],
"strings": [
"hoge",
"fuga",
"piyo"
]
}
== Deserialize ==
MyBean {
strings = [hoge, fuga, piyo]
ints = [1, 2, 3]
}
- 配列は JSON のリストにマッピングされる
ポリモーフィック型に対応させる
実装
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;
@JsonbTypeInfo({
@JsonbSubtype(alias = "Hoge", type = Hoge.class),
@JsonbSubtype(alias = "Fuga", type = Fuga.class)
})
public interface MyInterface {
}
package sandbox.jsonb;
public class Hoge implements MyInterface {
public String message = "hoge";
// toString省略
}
package sandbox.jsonb;
public class Fuga implements MyInterface {
public int number = 9;
// toString省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
実行結果
== Serialize ==
{
"mi1": {
"@type": "Hoge",
"message": "hoge"
},
"mi2": {
"@type": "Fuga",
"number": 9
}
}
== Deserialize ==
MyBean {
mi1 = Hoge {
message = "hoge"
}
mi2 = Fuga {
number = 9
}
}
説明
- ポリモーフィック(多態性)を用いたフィールドのマッピングを実現するには、
@JsonTypeInfoおよび@JsonSubtypeアノテーションを利用する
@JsonbTypeInfo({
@JsonbSubtype(alias = "Hoge", type = Hoge.class),
@JsonbSubtype(alias = "Fuga", type = Fuga.class)
})
public interface MyInterface {
- 親となる型に
@JsonTypeInfoアノテーションを付ける- さらに
valueに@JsonSubtypeアノテーションを設定してサブタイプの情報を定義する
- さらに
-
@JsonSubtypeにはaliasとtypeを設定する-
aliasには、このサブタイプを識別する値を指定する -
typeには、サブタイプのClassオブジェクトを指定する
-
- このアノテーションを設定すると、 JSON に型の情報が出力されるようになる
{
"mi1": {
"@type": "Hoge",
"message": "hoge"
},
"mi2": {
"@type": "Fuga",
"number": 9
}
}
-
@typeプロパティに、サブタイプの型を識別する値が出力されている-
@JsonSubtypeのaliasに指定した値
-
- デシリアライズ時は、この
@typeの情報と@JsonSubtypeの情報を用いて型が決定される -
@typeというプロパティ名は@JsonTypeInfoのkeyで変更できる
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbSubtype;
import jakarta.json.bind.annotation.JsonbTypeInfo;
@JsonbTypeInfo(
key = "$TYPE",
value = {
@JsonbSubtype(alias = "Hoge", type = Hoge.class),
@JsonbSubtype(alias = "Fuga", type = Fuga.class)
}
)
public interface MyInterface {
}
{
"mi1": {
"$TYPE": "Hoge",
"message": "hoge"
},
"mi2": {
"$TYPE": "Fuga",
"number": 9
}
}
マッピングの調整
マッピングの対象外にする
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbTransient;
public class MyBean {
public String normal = "normal";
@JsonbTransient
public String ignore = "ignore";
private String ignoreSerialize = "ignoreSerialize";
private String ignoreDeserialize = "ignoreDeserialize";
@JsonbTransient
public String getIgnoreSerialize() {
return ignoreSerialize;
}
public void setIgnoreSerialize(String ignoreSerialize) {
this.ignoreSerialize = ignoreSerialize;
}
public String getIgnoreDeserialize() {
return ignoreDeserialize;
}
@JsonbTransient
public void setIgnoreDeserialize(String ignoreDeserialize) {
this.ignoreDeserialize = ignoreDeserialize;
}
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson("""
{
"normal": "aaa",
"ignore": "bbb",
"ignoreSerialize": "ccc",
"ignoreDeserialize": "ddd"
}
""", MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
== Serialize ==
{
"ignoreDeserialize": "ignoreDeserialize",
"normal": "normal"
}
== Deserialize ==
MyBean {
normal = "aaa"
ignore = "ignore"
ignoreSerialize = "ccc"
ignoreDeserialize = "ignoreDeserialize"
}
- フィールドに
@JsonbTransientを付けた場合、シリアライズとデシリアライズの両方が対象外になる- アクセサ経由のときもフィールドに
@JsonbTransientを付けることが可能
- アクセサ経由のときもフィールドに
- Getter に
@JsonbTransientを付けた場合、シリアライズのみ対象外になる - Setter に
@JsonbTransientを付けた場合、デシリアライズのみ対象外になる
JSON のプロパティ名を変更する
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbProperty;
public class MyBean {
public String normal = "normal";
@JsonbProperty("textValue")
public String text = "TEXT";
private String onlySerialize = "onlySerialize";
private String onlyDeserialize = "onlyDeserialize";
@JsonbProperty("only_serialize")
public String getOnlySerialize() {
return onlySerialize;
}
public void setOnlySerialize(String onlySerialize) {
this.onlySerialize = onlySerialize;
}
public String getOnlyDeserialize() {
return onlyDeserialize;
}
@JsonbProperty("only_deserialize")
public void setOnlyDeserialize(String onlyDeserialize) {
this.onlyDeserialize = onlyDeserialize;
}
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson("""
{
"normal": "aaa",
"textValue": "bbb",
"onlySerialize": "ccc",
"only_deserialize": "ddd"
}
""", MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
== Serialize ==
{
"normal": "normal",
"onlyDeserialize": "onlyDeserialize",
"only_serialize": "onlySerialize",
"textValue": "TEXT"
}
== Deserialize ==
MyBean {
normal = "aaa"
text = "bbb"
onlySerialize = "ccc"
onlyDeserialize = "ddd"
}
-
@JsonbPropertyを設定してvalueにプロパティ名を指定することで、 JSON のプロパティ名を任意の値に変更できる - フィールドに
@JsonbPropertyを設定すると、シリアライズとデシリアライズの両方でプロパティ名を変更できる - Getter に
@JsonbPropertyを設定すると、シリアライズ時のみプロパティ名を変更できる - Setter に
@JsonbPropertyを設定すると、デシリアライズ時のみプロパティ名を変更できる
JSON プロパティ名とのマッピング戦略を変更する
実装
package sandbox.jsonb;
public class MyBean {
public String helloJsonBinding = "Hello JSON Binding";
public int intValue = 1;
// toString 省略
@Override
public String toString() {
return ToStringUtil.toString(this);
}
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.config.PropertyNamingStrategy;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
config.withPropertyNamingStrategy(
PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
実行結果
== Serialize ==
{
"hello_json_binding": "Hello JSON Binding",
"int_value": 1
}
== Deserialize ==
MyBean {
helloJsonBinding = "Hello JSON Binding"
intValue = 1
}
説明
config.withPropertyNamingStrategy(
PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES);
-
JsonbConfigのwithPropertyNamingStrategyで、プロパティ名のマッピング戦略を変更できる - マッピング戦略は PropertyNamingStrategy に定義された定数で指定できる
プロパティ名のマッピング戦略
| 定数 | 説明 | 例(Java <-> JSON) |
|---|---|---|
CASE_INSENSITIVE |
※詳細後述 | - |
IDENTITY |
変換無し |
propertyName <-> propertyName
|
LOWER_CASE_WITH_DASHES |
小文字のダッシュ区切り |
propertyName <-> property-name
|
LOWER_CASE_WITH_UNDERSCORES |
小文字のアンスコ区切り |
propertyName <-> property_name
|
UPPER_CAMEL_CASE |
大文字のキャメルケース |
propertyName <-> PropertyName
|
UPPER_CAMEL_CASE_WITH_SPACES |
大文字の空白区切りキャメルケース |
propertyName <-> Property Name
|
-
CASE_INSENSITIVEだけちょっと特殊で、シリアライズ時は Java での名前がそのまま JSON のプロパティ名として利用される - しかし、デシリアライズ時は大文字・小文字区別せずにマッピングが行われる
- つまり、 JSON の
PropertyNAMEという名前のプロパティが、 Java 側のpropertyNameというフィールドにマッピングされる
- つまり、 JSON の
null も出力させる
実装
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbNillable;
public class MyBean {
public String text = "TEXT";
@JsonbNillable
public String nullValue = null;
// toString 省略
}
実行結果
{
"nullValue": null,
"text": "TEXT"
}
説明
-
@JsonbNillableを付けると、そのフィールドの値がnullのときでも JSON にプロパティを出力させられるようになる -
@JsonbNillableはクラスにもつけることができ、その場合は全フィールドが対象になる - 全てのシリアライズで設定を有効にしたい場合は、
JsonbConfigで設定する方法がある
config.withNullValues(true);
-
withNullValuesにtrueを渡すことで、そのJsonbConfigを使って生成したJsonbによるシリアライズは、全てnull値のプロパティも出力するようになる
インスタンス生成の方法を指定する
実装
package sandbox.jsonb;
public class MyBean {
public Hoge hoge = new Hoge("hoge");
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbCreator;
import jakarta.json.bind.annotation.JsonbProperty;
public class Hoge {
private final String text;
@JsonbCreator
public Hoge(@JsonbProperty("text") String text) {
this.text = text;
}
public String getText() {
return text;
}
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
System.out.println(jsonb.fromJson("""
{
"hoge": {"text": "HOGE"}
}
""", MyBean.class));
System.out.println("------------------------------");
System.out.println(jsonb.fromJson("""
{
"hoge": {"unknown": "HOGE"}
}
""", MyBean.class));
}
}
}
実行結果
MyBean {
hoge = Hoge {
text = "HOGE"
}
}
------------------------------
MyBean {
hoge = Hoge {
text = null
}
}
説明
@JsonbCreator
public Hoge(@JsonbProperty("text") String text) {
this.text = text;
}
- デフォルトでは、デシリアライズ時にマッピング先となるクラスにはデフォルトコンストラクタが必須となる
- なければエラー
-
@JsonbCreatorアノテーションを使用すると、デフォルトコンストラクタ以外の方法でインスタンスの生成方法を定義できる -
@JsonbCreatorは、コンストラクタまたはファクトリメソッドに設定できる -
@JsonbCreatorは、1つのクラスに1つだけ設定できる- 複数設定した場合はエラー
-
@JsonbCreatorが設定されたコンストラクタまたはファクトリメソッドは、デシリアライズ時にインスタンス生成で利用されるようになる- このとき、 JSON 上のどのプロパティを引数で受け取るかを
@JsonbPropertyアノテーションで設定できる -
valueに受け取るプロパティの名前を指定する -
@JsonbCreatorの引数はデフォルトではオプション扱いになる- JSON 上に
@JsonbPropertyで指定されたプロパティが無い場合は、nullなどのデフォルト値が渡される(エラーにはならない)
- JSON 上に
- このとき、 JSON 上のどのプロパティを引数で受け取るかを
JsonbCreator の引数を必須扱いにする
実装
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
config.withCreatorParametersRequired(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
System.out.println(jsonb.fromJson("""
{
"hoge": {"text": "HOGE"}
}
""", MyBean.class));
System.out.println("------------------------------");
System.out.println(jsonb.fromJson("""
{
"hoge": {"unknown": "HOGE"}
}
""", MyBean.class));
}
}
}
実行結果
MyBean {
hoge = Hoge {
text = "HOGE"
}
}
------------------------------
Exception in thread "main" jakarta.json.bind.JsonbException:
Unable to deserialize property 'hoge' because of:
JsonbCreator parameter text is missing in json document.
説明
config.withCreatorParametersRequired(true);
-
JsonbConfigのwithCreatorParametersRequiredにtrueを設定すると、@JsonbCreatorの引数を必須扱いにできる -
@JsonbPropertyで指定されたプロパティが JSON に無い場合、デシリアライズ時にエラーとなる
日付フォーマットを変更する
実装
package sandbox.jsonb;
import jakarta.json.bind.annotation.JsonbDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
public class MyBean {
@JsonbDateFormat("uuuu/MM/dd HH:mm:ss.SSS")
public Date date = new Date();
@JsonbDateFormat("uuuu/MM/dd HH:mm:ss.SSS")
public LocalDateTime localDateTime = LocalDateTime.now();
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
実行結果
== Serialize ==
{
"date": "2024/09/23 14:20:49.598",
"localDateTime": "2024/09/23 23:20:49.605"
}
== Deserialize ==
MyBean {
date = Mon Sep 23 23:20:49 JST 2024
localDateTime = 2024-09-23T23:20:49.605
}
説明
@JsonbDateFormat("uuuu/MM/dd HH:mm:ss.SSS")
public Date date = new Date();
@JsonbDateFormat("uuuu/MM/dd HH:mm:ss.SSS")
public LocalDateTime localDateTime = LocalDateTime.now();
-
@JsonbDateFormatを設定することで、日付フォーマットを指定できる -
valueにはjava.time.format.DateTimeFormatterで使用できるフォーマット書式を指定する-
java.util.Dateやjava.util.CalendarもDateTimeFormatterの書式で指定できる
-
-
@JsonbDateFormatはクラスやパッケージにも指定可能で、その場合は配下に含まれる全てのプロパティが対象になる- クラスとフィールドの両方に
@JsonbDateFormatが設定されている場合は、より狭い範囲の設定が優先される(この場合フィールドの設定が優先される)
- クラスとフィールドの両方に
- 全ての処理で日付フォーマットを変更したい場合は、
JsonbConfigで設定する方法もある
config.withDateFormat("uuuu/MM/dd HH:mm:ss", Locale.getDefault());
-
JsonbConfigのwithDateFormatで、デフォルトの日付フォーマットを設定できる- 第一引数は、
java.time.format.DateTimeFormatterで使用できるフォーマットを指定する - 第二引数は、
Localeを指定する
- 第一引数は、
- なお、ここでの設定は全ての日付・日時項目のフォーマットで使われることになるので、フォーマットで指定した日時項目を持たない日付・時刻型を変換対象のフィールドに使用していると実行時エラーとなるので注意
- 例えばフォーマットを
uuuu/MM/dd HH:mm:ssと指定した場合、年月日・時分秒の項目を持つことが前提となる - もし
LocalTime型のフィールドが変換対象にあると、実行時にエラーとなる
- 例えばフォーマットを
バイト配列を Base64 でエンコードする
実装
package sandbox.jsonb;
public class MyBean {
public byte[] binary = {1, 2, 3, 4};
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.config.BinaryDataStrategy;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
config.withFormatting(true);
config.withBinaryDataStrategy(BinaryDataStrategy.BASE_64);
try (Jsonb jsonb = JsonbBuilder.create(config)) {
MyBean bean = new MyBean();
String json = jsonb.toJson(bean);
System.out.println("== Serialize ==");
System.out.println(json);
MyBean deserialized = jsonb.fromJson(json, MyBean.class);
System.out.println("== Deserialize ==");
System.out.println(deserialized);
}
}
}
実行結果
== Serialize ==
{
"binary": "AQIDBA=="
}
== Deserialize ==
MyBean {
binary = [1, 2, 3, 4]
}
説明
config.withBinaryDataStrategy(BinaryDataStrategy.BASE_64);
-
JsonbConfigのwithBinaryDataStrategyに BinaryDataStrategy で定義されている定数を指定することで、 byte 配列のシリアライズ・デシリアライズの方法を変更できる-
BASE_64- Baes64 でシリアライズ・デシリアライズが行われる
-
BASE_64_URL- URLセーフな Base64 でシリアライズ・デシリアライズが行われる
- 詳しくは Base64 の Javadoc を参照
-
BYTE- byte の配列としてシリアライズ・デシリアライズが行われる
- デフォルト
-
総称型のデシリアライズ
実装
package sandbox.jsonb;
public class MyBean {
public String message;
// toString 省略
}
package sandbox.jsonb;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JsonbMain {
public static void main(String[] args) throws Exception {
JsonbConfig config = new JsonbConfig();
try (Jsonb jsonb = JsonbBuilder.create(config)) {
Type listType =
new ArrayList<MyBean>() {}.getClass().getGenericSuperclass();
List<MyBean> list = jsonb.fromJson("""
[
{"message": "hoge"},
{"message": "fuga"},
{"message": "piyo"}
]
""", listType);
System.out.println(list);
System.out.println("=================================");
Type mapType =
new HashMap<String, MyBean>() {}.getClass().getGenericSuperclass();
Map<String, MyBean> map = jsonb.fromJson("""
{
"a": {"message": "A"},
"b": {"message": "B"},
"c": {"message": "C"}
}
""", mapType);
System.out.println(map);
}
}
}
実行結果
[MyBean{message='hoge'}, MyBean{message='fuga'}, MyBean{message='piyo'}]
=================================
{a=MyBean{message='A'}, b=MyBean{message='B'}, c=MyBean{message='C'}}
説明
List<MyBean> list = jsonb.fromJson("""
[
{"message": "hoge"},
{"message": "fuga"},
{"message": "piyo"}
]
""", listType);
-
List<E>やMap<K, V>のような総称型のオブジェクトに型を維持してデシリアライズする方法 - 単に
List<MyBean> list = jsonb.fromJson("...", List.class)のようにすると、型引数の部分の情報が渡せないので、リストの各要素はMapにデシリアライズされてしまう- デシリアライズ先の型の情報がなく
Object扱いになるので、Mapにデシリアライズされる - 受け取る引数を
List<MyBean>にしてもコンパイルは通るが、実体はMapなので実行時にキャストエラーが発生する
- デシリアライズ先の型の情報がなく
- 総称型の型引数の情報を渡すためには、
java.lang.reflect.Type型のオブジェクトを渡す必要がある-
Typeは簡単にインスタンス化できるものではないので、取り方にやや癖がある
-
Type listType =
new ArrayList<MyBean>() {}.getClass().getGenericSuperclass();
...
Type mapType =
new HashMap<String, MyBean>() {}.getClass().getGenericSuperclass();
- 最終的なマッピング先にしたい総称型の具象型を継承した匿名クラスのインスタンスを生成する(
new ArrayList<MyBean>() {},new HashMap<String, MyBean>() {}) - そして、生成したインスタンスの
getClass().getGenericSuperClass()を呼ぶことでTypeのインスタンスを取得する - 取得した
TypeをfromJsonまたはtoJsonの引数に渡すことで、総称型の変換ができるようになる
List<MyBean> list = jsonb.fromJson(..., listType);
...
Map<String, MyBean> map = jsonb.fromJson(..., mapType);