JSONデータの構造変換とは、既存のJSONオブジェクトまたは配列を所定のルールに基づいて再編成、フィルタリング、あるいはフィールドの再配置を行うことで、新たなデータ形状を生成する処理を指します。この技術は、異種システム間のAPI連携、ログデータの標準化、バッチ処理前のデータ前処理など、データパイプラインの設計において不可欠な役割を果たします。代表的な操作には、特定フィールドの抽出、条件に基づくデータフィルタリング、値のマッピングと結合、および計算ロジックの適用が含まれます。
オブジェクト間マッピングの基本概念
JSON構造変換の中でも、オブジェクトからオブジェクトへの変換(Object-to-Object)は頻出するパターンです。ソースデータのキー構造をターゲットシステムの期待する形状に合わせて再配置する際、単純なコピーだけでなく、キーの置換、値の追加、構造のオーバーライド、ネストされた要素の展開など、多様な制御が必要となります。
以下に、ソースJSON内のプロパティ`a`を、ターゲットJSONのプロパティ`b1`へ各種ルールで配置・変換する実装ケースを提示します。
ソースJSON構造:
{
"a": {
"k": "v",
"a": "b"
}
}
ターゲットJSON構造:
{
"b1": {
"k1": "v1"
}
}
実装例:プロパティの再配置と変換ロジック
上記の構造変換を柔軟に実装するため、マッピングルールと変換モードを定義し、Jacksonの`ObjectMapper`を基盤とした再利用可能な変換エンジンを作成します。変換モードは、キーの完全置換(Mode 1)、ターゲット値への追記(Mode 2)、値構造のオーバーライド(Mode 3)、ソース値のキー展開(Mode 4)、内部値の注入(Mode 5)、特定フィールド値での置換(Mode 6)の6種類を想定しています。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
public class JsonStructureRebuilder {
private static final ObjectMapper JSON_ENGINE = new ObjectMapper();
public static String transform(String sourcePayload, String targetSchema, List<String> mappingRules) throws Exception {
ObjectNode targetRoot = JSON_ENGINE.treeToValue(JSON_ENGINE.readTree(targetSchema), ObjectNode.class);
ObjectNode sourceRoot = JSON_ENGINE.treeToValue(JSON_ENGINE.readTree(sourcePayload), ObjectNode.class);
for (String rule : mappingRules) {
String[] segments = rule.split(":");
String destPath = segments[0];
String srcPath = segments[1];
int operationCode = Integer.parseInt(segments[2]);
ObjectNode srcNode = (ObjectNode) sourceRoot.get(srcPath.split("\\.")[1]);
ObjectNode dstNode = (ObjectNode) targetRoot.get(destPath.split("\\.")[0]);
if (srcNode == null || dstNode == null) continue;
switch (operationCode) {
case 1: // ターゲットキーをソース構造で置換
targetRoot.set(destPath.split("\\.")[0], srcNode);
break;
case 2: // ターゲット値内部にソースキーを追記
dstNode.set("appended_key", srcNode);
break;
case 3: // ターゲット値の構造を完全に上書き
targetRoot.set(destPath.split("\\.")[0], srcNode);
break;
case 4: // ソースの値をターゲットの新しいキーとして展開
targetRoot.set(srcPath.split("\\.")[1], srcNode);
break;
case 5: // ソースの特定値をターゲット内部に注入
dstNode.set("injected_val", srcNode.get("a"));
break;
case 6: // ターゲットキーをソースの特定フィールド値で置換
targetRoot.set("b1", srcNode.get("a"));
break;
}
}
return JSON_ENGINE.writeValueAsString(targetRoot);
}
public static void main(String[] args) {
String srcJson = "{\"a\":{\"k\":\"v\",\"a\":\"b\"}}";
String tgtJson = "{\"b1\":{\"k1\":\"v1\"}}";
try {
// Mode 1(キー置換)の実行例
String output = transform(srcJson, tgtJson, List.of("b1:a:1"));
System.out.println("変換結果: " + output);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上記のロジックでは、`mappingRules`リストに変換条件を文字列で定義し、実行時にパースして対応する`ObjectNode`操作を行います。モード1ではソースの階層構造がターゲットキーにそのまま置換され、モード2では既存のターゲット値に新たなフィールドが追記されます。モード3は値の完全上書き、モード4はキー名自体の再定義、モード5・6はネストされたプリミティブ値やオブジェクトの抽出と再配置に該当します。これらをルールベースで切り替えることで、複雑なJSONスキーマ間の変換を宣言的に記述することが可能になります。