JSONでの双方向の関係のサポート
公開: 2022-03-11双方向の関係(循環参照)を持つエンティティを含むJSONデータ構造を作成しようとしたことがありますか? ある場合は、 「Uncaught TypeError:Converting Circular StructuretoJSON」の行に沿ってJavaScriptエラーが発生している可能性があります。 または、Jacksonライブラリを使用するJava開発者の場合、 「JSONを記述できませんでした:ルート原因java.lang.StackOverflowErrorで無限再帰(StackOverflowError)」が発生した可能性があります。
この記事では、これらのエラーを発生させることなく、双方向の関係を含むJSON構造を作成するための堅牢な作業アプローチを提供します。
多くの場合、この問題に対して提示される解決策は、基本的に回避する回避策を伴いますが、実際には問題に対処しません。 例としては、 @JsonManagedReference
や@JsonBackReference
(シリアル化から後方参照を単純に省略)などのJacksonアノテーションタイプを使用したり、 @JsonIgnore
を使用して関係の片側を単に無視したりします。 あるいは、データ内のそのような双方向の関係や循環依存を無視するカスタムシリアル化コードを開発することもできます。
しかし、双方向の関係のどちらの側も無視したり省略したりしたくはありません。 エラーを発生させずに、両方向で保存したいと考えています。 実際のソリューションでは、JSONで循環依存関係を許可し、開発者が追加のアクションを実行せずにそれらについて考えるのをやめることができるようにする必要があります。 この記事では、これを行うための実用的でわかりやすい手法を提供します。これは、今日のフロントエンド開発者向けの標準的なヒントと実践のセットに役立つ追加機能として役立ちます。
単純な双方向関係の例
この双方向の関係(循環依存)の問題が発生する一般的なケースは、子を持つ親オブジェクト(それが参照する)があり、それらの子オブジェクトが親への参照を維持したい場合です。 簡単な例を次に示します。
var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]
上記のparent
オブジェクトをJSONに変換しようとすると(たとえば、 var parentJson = JSON.stringify(parent);
のようにstringify
メソッドを使用して)、例外Uncaught TypeError:Converting Circular StructuretoJSONがスローされます。
上記の手法の1つ( @JsonIgnore
などのアノテーションの使用など)を使用することも、子から親への上記の参照を削除することもできますが、これらは問題を解決するのではなく回避する方法です。 本当に必要なのは、各双方向の関係を維持し、例外をスローせずにJSONに変換できる結果のJSON構造です。
ソリューションへの移行
解決に向けた潜在的に明白なステップの1つは、各オブジェクトに何らかの形式のオブジェクトIDを追加してから、親オブジェクトへの子の参照を親オブジェクトのIDへの参照に置き換えることです。 例えば:
var obj = { "id": 100, "name": "I'm parent" } obj.children = [ { "id": 101, "name": "I'm first child", "parent": 100 }, { "id": 102, "name": "I'm second child", "parent": 100 } ]
このアプローチは、双方向の関係または循環参照から生じる例外を確実に回避します。 しかし、まだ問題があり、これらの参照をシリアル化および逆シリアル化する方法を考えると、その問題が明らかになります。
問題は、上記の例を使用して、値「100」へのすべての参照が親オブジェクトを参照していることを知る必要があることです(これはそのid
であるため)。 これは、値「100」を持つ唯一のプロパティがparent
プロパティである上記の例では問題なく機能します。 しかし、値が「100」の別のプロパティを追加するとどうなるでしょうか。 例えば:
obj.children = [ { "id": 101, "name": "I'm first child", "priority": 100, // This is NOT referencing object ID "100" "parent": 100 // This IS referencing object ID "100" }, { "id": 102, "name": "I'm second child", "priority": 200, "parent": 100 } ]
値「100」への参照がオブジェクトを参照していると仮定すると、 parent
が値「100」を参照している場合、それが親オブジェクトのid
を参照していることをシリアル化/逆シリアル化コードが知る方法はありませんが、 priority
は値「100」を参照します。これは親オブジェクトのid
を参照していません( priority
は親オブジェクトのid
も参照していると見なされるため、その値は誤って親オブジェクトへの参照に置き換えられます)。
この時点で、 「待ってください。明らかな解決策がありません。 プロパティ値を使用してオブジェクトIDを参照していると判断する代わりに、プロパティ名だけを使用してみませんか?」 確かに、それはオプションですが、非常に限定的なものです。 これは、常に他のオブジェクト(「親」、「子」、「次」などの名前)を参照すると想定される「予約済み」プロパティ名のリストを事前に指定する必要があることを意味します。 これは、それらのプロパティ名のみが他のオブジェクトへの参照に使用できることを意味し、それらのプロパティ名は常に他のオブジェクトへの参照として扱われることも意味します。 したがって、これはほとんどの状況で実行可能な代替手段ではありません。
したがって、プロパティ値をオブジェクト参照として認識することに固執する必要があるようです。 ただし、これは、これらの値が他のすべてのプロパティ値から一意であることを保証する必要があることを意味します。 グローバル一意識別子(GUID)を使用することで、一意の値の必要性に対処できます。 例えば:
var obj = { "id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent" } obj.children = [ { "id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "priority": 100, "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id }, { "id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "priority": 200, "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" // matches unique parent id } ]
だからそれはうまくいくはずですよね?
はい。
だが…
完全に自動化されたソリューション
私たちの最初の挑戦を忘れないでください。 例外を生成することなく、JSONとの間で双方向の関係を持つオブジェクトをシリアル化および逆シリアル化できるようにしたかったのです。 上記のソリューションはこれを実現しますが、(a)各オブジェクトに何らかの形式の一意のIDフィールドを追加し、(b)各オブジェクト参照を対応する一意のIDに置き換える必要があります。 これは機能しますが、この方法でオブジェクトを「手動で」変更しなくても、既存のオブジェクト参照を自動的に処理するソリューションの方がはるかに望ましいです。
理想的には、オブジェクトのセット(任意のプロパティとオブジェクト参照のセットを含む)をシリアライザーとデシリアライザーに(双方向の関係に基づいて例外を生成せずに)渡し、デシリアライザーによって生成されたオブジェクトを正確に一致させることができるようにする必要がありますシリアライザーに供給されたオブジェクト。
私たちのアプローチは、シリアライザーが自動的に一意のIDを作成し(GUIDを使用して)各オブジェクトに追加するようにすることです。 次に、オブジェクト参照をそのオブジェクトのGUIDに置き換えます。 (シリアライザーはこれらのIDにも一意のプロパティ名を使用する必要があることに注意してください。この例では、プロパティ名の前に「@」を付けるだけで一意であることを確認できるため、@ @id
を使用します。)デシリアライザー次に、オブジェクトIDに対応するGUIDをそのオブジェクトへの参照に置き換えます(デシリアライザーは、デシリアライザーによって生成されたGUIDもデシリアライズされたオブジェクトから削除し、それによってオブジェクトを正確に初期状態に戻すことに注意してください)。
したがって、例に戻って、次のオブジェクトのセットをそのままシリアライザーにフィードします。
var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]
次に、シリアライザーが次のようなJSON構造を生成することを期待します。
{ "@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent", "children": [ { "@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" }, { "@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" }, ] }
(JSONフォーマッターツールを使用して、任意のJSONオブジェクトをきれいにすることができます。)
次に、上記のJSONをデシリアライザーにフィードすると、元のオブジェクトのセットが生成されます(つまり、親オブジェクトとその2つの子、相互に適切に参照します)。
何をしたいのか、どのようにしたいのかがわかったので、実装しましょう。
JavaScriptでのシリアライザーの実装
以下は、例外をスローせずに双方向の関係を適切に処理するシリアライザーのJavaScript実装のサンプルです。
var convertToJson = function(obj) { // Generate a random value structured as a GUID var guid = function() { function s4() { return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); } return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); }; // Check if a value is an object var isObject = function(value) { return (typeof value === 'object'); } // Check if an object is an array var isArray = function(obj) { return (Object.prototype.toString.call(obj) === '[object Array]'); } var convertToJsonHelper = function(obj, key, objects) { // Initialize objects array and // put root object into if it exist if(!objects) { objects = []; if (isObject(obj) && (! isArray(obj))) { obj[key] = guid(); objects.push(obj); } } for (var i in obj) { // Skip methods if (!obj.hasOwnProperty(i)) { continue; } if (isObject(obj[i])) { var objIndex = objects.indexOf(obj[i]); if(objIndex === -1) { // Object has not been processed; generate key and continue // (but don't generate key for arrays!) if(! isArray(obj)) { obj[i][key] = guid(); objects.push(obj[i]); } // Process child properties // (note well: recursive call) convertToJsonHelper(obj[i], key, objects); } else { // Current object has already been processed; // replace it with existing reference obj[i] = objects[objIndex][key]; } } } return obj; } // As discussed above, the serializer needs to use some unique property name for // the IDs it generates. Here we use "@id" since presumably prepending the "@" to // the property name is adequate to ensure that it is unique. But any unique // property name can be used, as long as the same one is used by the serializer // and deserializer. // // Also note that we leave off the 3rd parameter in our call to // convertToJsonHelper since it will be initialized within that function if it // is not provided. return convertToJsonHelper(obj, "@id"); }
JavaScriptでのデシリアライザーの実装
以下は、例外をスローせずに双方向の関係を適切に処理するデシリアライザーのJavaScript実装のサンプルです。

var convertToObject = function(json) { // Check if an object is an array var isObject = function(value) { return (typeof value === 'object'); } // Iterate object properties and store all reference keys and references var getKeys = function(obj, key) { var keys = []; for (var i in obj) { // Skip methods if (!obj.hasOwnProperty(i)) { continue; } if (isObject(obj[i])) { keys = keys.concat(getKeys(obj[i], key)); } else if (i === key) { keys.push( { key: obj[key], obj: obj } ); } } return keys; }; var convertToObjectHelper = function(json, key, keys) { // Store all reference keys and references to object map if(!keys) { keys = getKeys(json, key); var convertedKeys = {}; for(var i = 0; i < keys.length; i++) { convertedKeys[keys[i].key] = keys[i].obj; } keys = convertedKeys; } var obj = json; // Iterate all object properties and object children // recursively and replace references with real objects for (var j in obj) { // Skip methods if (!obj.hasOwnProperty(j)) { continue; } if (isObject(obj[j])) { // Property is an object, so process its children // (note well: recursive call) convertToObjectHelper(obj[j], key, keys); } else if( j === key) { // Remove reference id delete obj[j]; } else if (keys[obj[j]]) { // Replace reference with real object obj[j] = keys[obj[j]]; } } return obj; }; // As discussed above, the serializer needs to use some unique property name for // the IDs it generates. Here we use "@id" since presumably prepending the "@" to // the property name is adequate to ensure that it is unique. But any unique // property name can be used, as long as the same one is used by the serializer // and deserializer. // // Also note that we leave off the 3rd parameter in our call to // convertToObjectHelper since it will be initialized within that function if it // is not provided. return convertToObjectHelper(json, "@id"); }
これらの2つのメソッドを介してオブジェクトのセット(双方向の関係を持つオブジェクトを含む)を渡すことは、本質的に恒等関数です。 つまり、 convertToObject(convertToJson(obj)) === obj
はtrueと評価されます。
Java/Jacksonの例
次に、このアプローチが一般的な外部ライブラリでどのようにサポートされているかを見てみましょう。 たとえば、Jacksonライブラリを使用してJavaでどのように処理されるかを見てみましょう。
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id") public class Parent implements Serializable { private String name; private List<Child> children = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Child> getChildren() { return children; } public void setChildren(List<Child> children) { this.children = children; } } @JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="@id") public class Child implements Serializable { private String name; private Parent parent; public String getName() { return name; } public void setName(String name) { this.name = name; } public Parent getParent() { return parent; } public void setParent(Parent parent) { this.parent = parent; } }
これらの2つのJavaクラスParentとChildは、この記事の冒頭のJavaScriptの例と同じ構造を表しています。 ここでの主なポイントは、 @JsonIdentityInfo
アノテーションを使用して、これらのオブジェクトをシリアル化/逆シリアル化する方法をジャクソンに指示することです。
例を見てみましょう:
Parent parent = new Parent(); parent.setName("I'm parent") Child child1 = new Child(); child1.setName("I'm first child"); Child child2 = new Child(); child2.setName("I'm second child"); parent.setChildren(Arrays.asList(child1, child2));
親インスタンスをJSONにシリアル化した結果、JavaScriptの例と同じJSON構造が返されます。
{ "@id": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc", "name": "I'm parent", "children": [ { "@id": "6616c598-0a0a-8263-7a56-fb0c0e16225a", "name": "I'm first child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" }, { "@id": "940e60e4-9497-7c0d-3467-297ff8bb9ef2", "name": "I'm second child", "parent": "28dddab1-4aa7-6e2b-b0b2-7ed9096aa9bc" }, ] }
別の利点
JSONで双方向の関係を処理するための説明されたアプローチは、同じオブジェクトの冗長コピーを含める必要がなく、一意のIDだけでオブジェクトを参照できるため、JSONファイルのサイズを縮小するのにも役立ちます。
次の例を考えてみましょう。
{ "@id": "44f47be7-af77-9a5a-8606-a1e6df299ec9", "id": 1, "name": "I'm parent", "children": [ { "@id": "54f47be7-af77-9a5a-8606-a1e6df299eu8", "id": 10, "name": "I'm first child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9" }, { "@id": "98c47be7-af77-9a5a-8606-a1e6df299c7a", "id": 11, "name": "I'm second child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9" }, { "@id": "5jo47be7-af77-9a5a-8606-a1e6df2994g2", "id": 11, "name": "I'm third child", "parent": "44f47be7-af77-9a5a-8606-a1e6df299ec9" } ], "filteredChildren": [ "54f47be7-af77-9a5a-8606-a1e6df299eu8", "5jo47be7-af77-9a5a-8606-a1e6df2994g2" ] }
filteredChildren
配列に示されているように、参照されるオブジェクトとそのコンテンツのレプリカではなく、JSONにオブジェクト参照を含めることができます。
要約
このソリューションを使用すると、オブジェクトとデータへの制約を最小限に抑える方法でJSONファイルをシリアル化しながら、循環参照関連の例外を排除できます。 JSONファイルのシリアル化を処理するために使用しているライブラリでそのようなソリューションがまだ利用できない場合は、提供されている実装例に基づいて独自のソリューションを実装できます。 これがお役に立てば幸いです。