Dukungan Hubungan Dua Arah di JSON
Diterbitkan: 2022-03-11Pernah mencoba membuat struktur data JSON yang menyertakan entitas yang memiliki hubungan dua arah (yaitu, referensi melingkar)? Jika pernah, Anda mungkin pernah melihat kesalahan JavaScript di sepanjang baris “Uncaught TypeError: Converting circular structure to JSON” . Atau jika Anda seorang pengembang Java yang menggunakan perpustakaan Jackson, Anda mungkin menemukan “Could not write JSON: Infinite recursion (StackOverflowError) with root cause java.lang.StackOverflowError” .
Artikel ini memberikan pendekatan kerja yang kuat untuk membuat struktur JSON yang menyertakan hubungan dua arah tanpa mengakibatkan kesalahan ini.
Seringkali, solusi yang disajikan untuk masalah ini memerlukan solusi yang pada dasarnya mengesampingkan, tetapi tidak benar-benar mengatasi masalah tersebut. Contohnya termasuk menggunakan jenis anotasi Jackson seperti @JsonManagedReference
dan @JsonBackReference
(yang hanya menghilangkan referensi belakang dari serialisasi) atau menggunakan @JsonIgnore
untuk mengabaikan salah satu sisi hubungan. Atau, seseorang dapat mengembangkan kode serialisasi khusus yang mengabaikan hubungan dua arah atau ketergantungan melingkar dalam data.
Namun kami tidak ingin mengabaikan atau menghilangkan salah satu sisi dari hubungan dua arah tersebut. Kami ingin mempertahankannya, di kedua arah, tanpa menghasilkan kesalahan apa pun. Solusi nyata harus memungkinkan dependensi melingkar di JSON dan memungkinkan pengembang berhenti memikirkannya tanpa mengambil tindakan tambahan untuk memperbaikinya. Artikel ini memberikan teknik praktis dan langsung untuk melakukannya, yang dapat berfungsi sebagai tambahan yang berguna untuk kumpulan tip dan praktik standar apa pun untuk pengembang front-end saat ini.
Contoh Hubungan Dua Arah Sederhana
Kasus umum di mana masalah hubungan dua arah (alias ketergantungan melingkar) ini muncul adalah ketika ada objek induk yang memiliki anak (yang dirujuk) dan objek anak tersebut, pada gilirannya, ingin mempertahankan referensi ke induknya. Berikut ini contoh sederhana:
var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]
Jika Anda mencoba mengonversi objek parent
di atas ke JSON (misalnya, dengan menggunakan metode stringify
, seperti dalam var parentJson = JSON.stringify(parent);
), pengecualian Uncaught TypeError: Converting circular structure to JSON akan dilempar.
Meskipun kita dapat menggunakan salah satu teknik yang dibahas di atas (seperti menggunakan anotasi seperti @JsonIgnore
), atau kita dapat dengan mudah menghapus referensi di atas untuk orang tua dari anak-anak, ini adalah cara untuk menghindari daripada menyelesaikan masalah. Yang benar-benar kami inginkan adalah struktur JSON yang dihasilkan yang mempertahankan setiap hubungan dua arah dan yang dapat kami konversi ke JSON tanpa membuang pengecualian apa pun.
Bergerak Menuju Solusi
Salah satu langkah yang berpotensi jelas menuju solusi adalah menambahkan beberapa bentuk ID objek ke setiap objek dan kemudian mengganti referensi anak-anak ke objek induk dengan referensi ke id objek induk. Sebagai contoh:
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 } ]
Pendekatan ini tentu akan menghindari pengecualian yang dihasilkan dari hubungan dua arah atau referensi melingkar. Tapi masih ada masalah, dan masalah itu menjadi jelas ketika kita berpikir tentang bagaimana kita akan membuat serialisasi dan deserializing referensi ini.
Masalahnya adalah kita perlu mengetahui, dengan menggunakan contoh di atas, bahwa setiap referensi ke nilai "100" mengacu pada objek induk (karena itu adalah id
nya ). Itu akan berfungsi dengan baik dalam contoh di atas di mana satu-satunya properti yang memiliki nilai "100" adalah properti parent
. Tapi bagaimana jika kita menambahkan properti lain dengan nilai “100”? Sebagai contoh:
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 } ]
Jika kita berasumsi bahwa referensi apa pun ke nilai "100" mereferensikan objek, tidak akan ada cara bagi kode serialisasi/deserialisasi kami untuk mengetahui bahwa ketika parent
mereferensikan nilai "100", itu adalah referensi id
objek induk, tetapi ketika priority
mereferensikan nilai "100", yang BUKAN mereferensikan id
objek induk (dan karena ia akan berpikir bahwa priority
juga merujuk id
objek induk, ia akan salah mengganti nilainya dengan referensi ke objek induk).
Anda mungkin bertanya pada titik ini, “Tunggu, Anda melewatkan solusi yang jelas. Alih-alih menggunakan nilai properti untuk menentukan bahwa itu merujuk pada id objek, mengapa Anda tidak menggunakan nama properti saja?” Memang, itu adalah pilihan, tetapi sangat membatasi. Ini berarti bahwa kita perlu menetapkan terlebih dahulu daftar nama properti "cadangan" yang selalu dianggap merujuk objek lain (nama seperti "induk", "anak", "berikutnya", dll.). Ini berarti bahwa hanya nama properti tersebut yang dapat digunakan untuk referensi ke objek lain dan juga berarti bahwa nama properti tersebut akan selalu diperlakukan sebagai referensi ke objek lain. Oleh karena itu, ini bukan alternatif yang layak dalam kebanyakan situasi.
Jadi sepertinya kita harus tetap mengenali nilai properti sebagai referensi objek. Tetapi ini berarti bahwa kita membutuhkan nilai-nilai ini untuk dijamin unik dari semua nilai properti lainnya. Kita dapat mengatasi kebutuhan akan nilai unik dengan menggunakan Pengidentifikasi Unik Global (GUID). Sebagai contoh:
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 } ]
Jadi itu harus berhasil, kan?
Ya.
Tetapi…
Solusi Sepenuhnya Otomatis
Ingat tantangan awal kita. Kami ingin dapat membuat serial dan deserialize objek yang memiliki hubungan dua arah ke/dari JSON tanpa menghasilkan pengecualian apa pun. Sementara solusi di atas menyelesaikan ini, ia melakukannya dengan mengharuskan kita untuk (a) menambahkan beberapa bentuk bidang ID unik ke setiap objek dan (b) mengganti setiap referensi objek dengan ID unik yang sesuai. Ini akan berhasil, tetapi kami lebih memilih solusi yang hanya akan bekerja secara otomatis dengan referensi objek yang ada tanpa mengharuskan kami untuk "secara manual" memodifikasi objek kami dengan cara ini.
Idealnya, kita ingin dapat melewatkan satu set objek (berisi sembarang set properti dan referensi objek) melalui serializer dan deserializer (tanpa menghasilkan pengecualian apa pun berdasarkan hubungan dua arah) dan membuat objek yang dihasilkan oleh deserializer benar-benar cocok objek yang dimasukkan ke dalam serializer.
Pendekatan kami adalah membuat serializer kami secara otomatis membuat dan menambahkan ID unik (menggunakan GUID) ke setiap objek. Itu kemudian menggantikan referensi objek apa pun dengan GUID objek itu. (Perhatikan bahwa serializer perlu menggunakan beberapa nama properti unik untuk ID ini juga; dalam contoh kami, kami menggunakan @id
karena mungkin menambahkan "@" ke nama properti sudah cukup untuk memastikan bahwa itu unik.) Deserializer kemudian akan mengganti GUID apa pun yang sesuai dengan ID objek dengan referensi ke objek itu (perhatikan bahwa deserializer juga akan menghapus GUID yang dihasilkan serializer dari objek yang dideserialisasi, sehingga mengembalikannya secara tepat ke keadaan awalnya).
Jadi kembali ke contoh kami, kami ingin memberi makan set objek berikut seperti ke serializer kami:
var obj = { "name": "I'm parent" } obj.children = [ { "name": "I'm first child", "parent": obj }, { "name": "I'm second child", "parent": obj } ]
Kami kemudian mengharapkan serializer untuk menghasilkan struktur JSON yang mirip dengan yang berikut:
{ "@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" }, ] }
(Anda dapat menggunakan alat formatter JSON untuk mempercantik objek JSON apa pun.)

Kemudian memberi makan JSON di atas ke deserializer akan menghasilkan kumpulan objek asli (yaitu, objek induk dan dua anaknya, saling merujuk dengan benar).
Jadi sekarang kita tahu apa yang ingin kita lakukan dan bagaimana kita ingin melakukannya, mari kita implementasikan.
Menerapkan Serializer dalam JavaScript
Di bawah ini adalah contoh implementasi JavaScript yang berfungsi dari serializer yang akan menangani hubungan dua arah dengan benar tanpa membuang pengecualian apa pun.
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"); }
Menerapkan Deserializer dalam JavaScript
Di bawah ini adalah contoh implementasi JavaScript yang berfungsi dari deserializer yang akan menangani hubungan dua arah dengan benar tanpa membuang pengecualian apa pun.
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"); }
Melewati satu set objek (termasuk yang memiliki hubungan dua arah) melalui dua metode ini pada dasarnya adalah fungsi identitas; yaitu, convertToObject(convertToJson(obj)) === obj
bernilai true.
Contoh Java/Jackson
Sekarang mari kita lihat bagaimana pendekatan ini didukung di perpustakaan eksternal populer. Sebagai contoh, mari kita lihat bagaimana penanganannya di Java menggunakan perpustakaan Jackson.
@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; } }
Kedua kelas Java Parent dan Child ini mewakili struktur yang sama seperti pada contoh JavaScript di awal artikel ini. Poin utama di sini adalah menggunakan anotasi @JsonIdentityInfo
yang akan memberi tahu Jackson cara membuat serial/deserialisasi objek-objek ini.
Mari kita lihat contohnya:
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));
Sebagai hasil dari serialisasi instance induk ke JSON, struktur JSON yang sama akan dikembalikan seperti pada contoh JavaScript.
{ "@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" }, ] }
Keuntungan lain
Pendekatan yang dijelaskan untuk menangani hubungan dua arah di JSON juga dapat dimanfaatkan untuk membantu mengurangi ukuran file JSON, karena memungkinkan Anda untuk mereferensikan objek hanya dengan ID uniknya, daripada perlu menyertakan salinan berlebihan dari objek yang sama.
Perhatikan contoh berikut:
{ "@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" ] }
Seperti yang ditunjukkan dalam larik filteredChildren
, kita cukup menyertakan referensi objek di JSON daripada replika objek yang direferensikan dan kontennya.
Bungkus
Dengan solusi ini, Anda dapat menghilangkan pengecualian terkait referensi melingkar saat membuat serial file JSON dengan cara yang meminimalkan kendala apa pun pada objek dan data Anda. Jika tidak ada solusi seperti itu yang sudah tersedia di pustaka yang Anda gunakan untuk menangani serialisasi file JSON, Anda dapat mengimplementasikan solusi Anda sendiri berdasarkan contoh implementasi yang disediakan. Harap Anda menemukan ini bermanfaat.