JSON 中的雙向關係支持

已發表: 2022-03-11

曾經嘗試過創建包含具有雙向關係(即循環引用)的實體的 JSON 數據結構嗎? 如果您有,您可能已經看到類似“未捕獲的類型錯誤:將循環結構轉換為 JSON”的 JavaScript 錯誤。 或者,如果您是使用 Jackson 庫的 Java 開發人員,您可能遇到過“Could not write JSON: Infinite recursion (StackOverflowError) with root cause java.lang.StackOverflowError”

JSON 雙向關係挑戰

本文提供了一種強大的工作方法來創建包含雙向關係的 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(例如,使用stringify方法,如var parentJson = JSON.stringify(parent); ),則會拋出異常Uncaught TypeError: Converting circular structure to JSON

雖然我們可以使用上面討論的技術之一(例如使用@JsonIgnore類的註釋),或者我們可以簡單地從子級中刪除上述對父級的引用,但這些都是避免而不是解決問題的方法。 我們真正想要的是一個生成的 JSON 結構,它維護每個雙向關係,並且我們可以在不拋出任何異常的情況下轉換為 JSON。

邁向解決方案

邁向解決方案的一個可能顯而易見的步驟是向每個對象添加某種形式的對象 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 提供給反序列化器將生成原始對象集(即,父對象及其兩個子對象,正確地相互引用)。

所以現在我們知道我們想要做什麼以及我們想要如何做,讓我們實現它。

在 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"); }

通過這兩種方法傳遞一組對象(包括具有雙向關係的對象)本質上是一個恆等函數; 即, convertToObject(convertToJson(obj)) === obj計算結果為真。

Java/傑克遜示例

現在讓我們看看流行的外部庫是如何支持這種方法的。 例如,讓我們看看如何使用 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; } }

這兩個 java 類ParentChild表示的結構與本文開頭的 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 中處理雙向關係的方法也可以用來幫助減小 JSON 文件的大小,因為它使您能夠簡單地通過對象的唯一 ID 來引用對象,而不需要包含同一對象的冗余副本。

考慮以下示例:

 { "@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 文件序列化的庫中還沒有此類解決方案,您可以根據提供的示例實現來實現您自己的解決方案。 希望你覺得這很有幫助。