springというかJacksonにおいて、Java側の子オブジェクトのプロパティを親階層のものとして扱いたい場合は@JsonUnwrappedを使用する。
たとえば、下記のようなjsonをJavaオブジェクト階層へ変換したい場合に使用する。
{
"id": "id001",
"start": "100",
"end": "200"
}
@Data
public class MyRoot {
String id;
Range range;
}
@Data
public class Range {
String start;
String end;
}
環境
厳密にはJacksonだけの話だがおおむねspring-bootと一緒に使う事が大多数と思われるのでbuild.gradleをそのまま載せている。
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.9'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
JsonUnwrappedの使い方
POJO
親オブジェクト側へプロパティを展開させたい子オブジェクトに@JsonUnwrappedを付与する。
@Data
public class MyRoot {
String id;
@JsonUnwrapped
Range range;
}
上記に相当するjsonは以下となる。
{
"id": "id001",
"start": "100",
"end": "200"
}
以下は動作確認用のサンプルコード。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class SampleUnrapperdMain {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
MyRoot value = mapper.readValue("""
{
"id": "id001",
"start": "100",
"end": "200"
}
""", MyRoot.class);
System.out.println(value);
}
}
以下が実行結果。
MyRoot(id=id001, range=Range(start=100, end=200))
record
recordも同様。ただ、俺自身がまだrecordの使用経験が少ないからもあるとは思うんだが、微妙に使い勝手が異なるように見える。
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
public record MyRootRecord(
String id,
@JsonUnwrapped
@JsonIgnore
RangeRecord range) {}
public record RangeRecord(String start, String end) {}
@JsonIgnoreを付与している点に注意。
以下は動作確認用のサンプルコード。
MyRootRecord value1 = mapper.readValue("""
{
"id": "id001",
"range": {
"start": "100",
"end": "200"
}
}
""", MyRootRecord.class);
System.out.println(value1);
MyRootRecord value2 = mapper.readValue("""
{
"id": "id001",
"start": "100",
"end": "200"
}
""", MyRootRecord.class);
System.out.println(value2);
以下が実行結果。
MyRootRecord[id=id001, range=RangeRecord[start=null, end=null]]
MyRootRecord[id=id001, range=RangeRecord[start=100, end=200]]
@JsonIgnoreを付与しない場合は両方とも同じ結果になってしまう。プロパティベースとコンストラクタベースでは微妙に使い勝手が異なる、のだろうか。