تحويل JSON إلى بنية Rust: توليد بنى serde (دليل 2026)
الصق حمولتك (payload) في محوّل JSON إلى Rust وانسخ بنية serde التي يولّدها. هذا هو كامل سير عمل تحويل JSON إلى بنية Rust: لا شيء لتثبيته، ولا شيء يُرفَع، وكل شيء يجري داخل متصفحك. إنه يغطّي حالة «أحتاج بنية الآن» في ثوانٍ.
لكن توليد بنية وتوليد البنية الصحيحة مشكلتان مختلفتان. لا يملك المحوّل إلا أن يخمّن. وهنا ترفع Rust الرهان. في TypeScript يتدهور التخمين الخاطئ إلى نوع any فضفاض يمكنك تجاهله. أما مع serde فالتخمين الخاطئ يفشل عند فك التسلسل: قيمة عشرية تُقحَم في i64، أو null غير متوقعة في حقل غير اختياري، فيسلّمك serde_json::from_str قيمة Err بدلًا من بياناتك. لهذا فإن إصابة البنية الصحيحة تهمّ في Rust أكثر من معظم اللغات.
في ما يلي كيف يعمل الاستنتاج فعليًا، وقاعدة تحديد أنواع الأرقام التي يخطئ فيها معظم المحوّلات، ومتى تلجأ إلى #[serde(rename)] مقابل #[serde(rename_all)]، وكيف تعالج التواريخ والمفاتيح الديناميكية وكلمات Rust المحجوزة حتى تُصرَّف المخرجات دائمًا بنجاح.
كيفية تحويل JSON إلى Rust
يستغرق تحويل JSON إلى Rust ثلاث خطوات:
- الصق الـ JSON. أسقِط كائنًا أو مصفوفة أو استجابة API خامًا في صندوق الإدخال. يجري التحويل فورًا وبالكامل من جانب العميل (client-side).
- اضبط المخرجات. أعد تسمية البنية الجذر، وبدّل اشتقاقات serde، وأضِف Debug وClone، أو أسقِط رؤية pub لتطابق أسلوب حزمتك (crate).
- انسخ أو نزّل. احصل على شفرة Rust المولّدة بنقرة واحدة والصقها مباشرة في مشروعك.
هذا هو كامل المسار العملي. إذا كان إدخالك مضغوطًا أو لست متأكدًا من صحته، فمرّره أولًا عبر منسّق JSON كي يعمل المحوّل على JSON نظيف وسليم البنية. تشرح بقية هذا الدليل كيف تقرأ المخرجات حتى تتمكن من إصلاح الحالات التي لا تستطيع الأداة استنتاجها وحدها.
كيف تُطابِق أنواع JSON أنواع Rust
لكل قيمة JSON نظير في Rust، لكن المطابقة ليست واحدًا لواحد تمامًا:
| قيمة JSON | نوع Rust |
|---|---|
"text" | String |
42 | i64 (القيم الكبيرة u64، وما بعدها f64) |
3.14, 2e3 | f64 |
true / false | bool |
null | Option<T> (أو Option<serde_json::Value>) |
[1, 2, 3] | Vec<T> |
{ ... } | بنية struct مسمّاة |
الكائنات هي موضع العمل الفعلي. خذ حمولة REST نموذجية:
{
"id": 101,
"name": "Ada Lovelace",
"email": "ada@example.com",
"active": true,
"roles": ["admin", "user"]
}
يُنتج المحوّل بنية جاهزة لـ serde:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
تُطابَق كل قيمة قياسية نوعًا أوليًا، وتصبح roles من النوع Vec<String>. النوع الوحيد في JSON الذي لا يُطابَق بسلاسة هو number: إذ يملك JSON نوعًا رقميًا واحدًا، بينما تفرض عليك Rust الاختيار بين i64 وu64 وf64. هذا الاختيار موضوع قسم كامل أدناه، لأنه حيث يخطئ معظم المولّدات.
كيف يستنتج المحوّل البنى
أربع قواعد تغطّي تقريبًا كل ما ستُغذّيه به: بنية واحدة لكل شكل كائن، ودمج مفتاحًا بمفتاح للمصفوفات، وتحديد دقيق لأنواع الأرقام، وأسماء حقول اصطلاحية.
الاستنتاج البنيوي: بنية واحدة مسمّاة لكل كائن
يصبح كل شكل كائن متمايز بنيةً مسمّاة خاصة به. الكائنات المتداخلة لا تُدمج مضمّنة، بل تُرفع إلى تعريفات منفصلة ومُشار إليها:
{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub repo: String,
pub owner: Owner,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
pub login: String,
pub id: i64,
}
يصبح owner المتداخل بنيةَ Owner خاصة به، مُشارًا إليها عبر الحقل. تُزال تكرارات الأشكال المتطابقة، فيتشارك حقلان لهما البنية نفسها بنيةً واحدة بدلًا من إنتاج نسخ.
دمج المصفوفات وحقول Option
عندما تمرّر مصفوفة من الكائنات، يدمجها المحوّل مفتاحًا بمفتاح. المفتاح الحاضر في بعض العناصر دون غيرها يصبح Option:
{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub users: Vec<User>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: i64,
pub nick: Option<String>,
}
id يظهر في كل عنصر، لذا يبقى مطلوبًا. nick غائب عن المستخدم الثاني، لذا يصبح Option<String>. لاحظ ما ليس موجودًا هنا: لا #[serde(default)]. فـ serde يعامل Option أصلًا على أنه اختياري ويفكّ المفتاح الغائب إلى None، لذا يكون الوسم زائدًا.
تحديد أنواع الأرقام الصحيح: i64 وu64 وf64
هذه هي القاعدة التي تتخطّاها الأدوات المنافِسة، وهي التي تنهار على الحمولات الحقيقية. يطبّق المولّد ثلاث طبقات. العدد الصحيح يُطابَق i64. إذا تجاوزت القيمة i64::MAX، تُرقَّى إلى u64. وبعد u64::MAX، تتراجع إلى f64. أي رمز مكتوب بفاصلة عشرية أو بأُس، مثل 1.0 أو 2e3، يُطابَق f64 بغضّ النظر عن حجمه.
{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub user_id: u64,
pub balance: f64,
pub retries: i64,
}
هنا user_id أكبر من i64::MAX، لذا يستقر في u64 ويظل يذهب ويعود دون فقد. balance يحمل فاصلة عشرية، لذا هو f64، لا عدد صحيح. وهذا يهمّ لأن serde يفرض النوع عند فك التسلسل. سلّمه بنية تُحدِّد نوع معرّف من نوع snowflake أو بأسلوب Twitter على أنه i32 فتفيض القيمة؛ وحدِّد نوع حقل عشري على أنه i64 فيعيد serde خطأً بدل أن يقتطع بصمت. قاعدة الطبقات الثلاث هي ما يُبقي بنية Rust المولّدة من JSON (rust struct from json) تُفكّ تسلسلًا بدل أن تنهار (panic) عند أول معرّف كبير.
حقول snake_case و#[serde(rename)]
غالبًا ما تكون مفاتيح JSON بنمط camelCase؛ أما حقول Rust الاصطلاحية فهي snake_case. يعيد المحوّل تسمية الحقل ويضيف #[serde(rename)] يُعيد ربطه بمفتاح JSON بالضبط:
{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub login: String,
#[serde(rename = "publicRepos")]
pub public_repos: i64,
#[serde(rename = "followerCount")]
pub follower_count: i64,
#[serde(rename = "createdAt")]
pub created_at: String,
}
المفتاح الذي هو أصلًا snake_case، مثل login، لا يحصل على إعادة تسمية. أما البقية فتُعيد الربط إلى صيغة الشبكة بينما تُقرأ شفرة Rust لديك بشكل طبيعي.
#[serde(rename)] مقابل #[serde(rename_all)]
تُصدر الأداة #[serde(rename)] لكل حقل لأنه يعمل دائمًا، حتى حين تخلط حمولة واحدة بين camelCase وsnake_case ومفاتيح غير منتظمة في الكائن نفسه. فلا يتعيّن عليها مطلقًا أن تفترض اصطلاحًا مشتركًا قد لا يوجد.
لكن حين تتشارك فعلًا كل حقول البنية اصطلاحًا واحدًا، يمكنك طيّ تلك الأوسمة في إعادة تسمية واحدة على مستوى الحاوية. احذف الأسطر المخصّصة لكل حقل وضع #[serde(rename_all = "camelCase")] واحدًا على البنية:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub login: String,
pub public_repos: i64,
pub follower_count: i64,
pub created_at: String,
}
كلتا النسختين تفكّان تسلسل الـ JSON نفسه. استخدم rename لكل حقل مع المفاتيح المختلطة أو غير المنتظمة؛ والجأ إلى rename_all حين تكون البنية متجانسة وتريد ضجيجًا أقل. يوفّر serde أيضًا اصطلاحات أخرى مثل "snake_case" و"kebab-case" و"SCREAMING_SNAKE_CASE" للوسم نفسه.
كلمات Rust المحجوزة والمفاتيح غير المعرِّفة
مفاتيح JSON مجرد نصوص، فلا شيء يمنع API من إرسال مفتاح باسم type أو match، وكلاهما كلمة محجوزة في Rust. يُنقّي المولّد هذه إلى معرّف قانوني مع إعادة تسمية:
{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
#[serde(rename = "type")]
pub type_: String,
#[serde(rename = "match")]
pub match_: bool,
#[serde(rename = "first-name")]
pub first_name: String,
}
صيغة الشرطة السفلية اللاحقة (type_) مُختارة عمدًا بدل معرّف خام مثل r#type. فالمعرّفات الخام لا تستطيع التعبير عن كل كلمة محجوزة في كل موضع؛ فـ self وcrate وsuper تحديدًا تُرفَض كمعرّفات خام، لذا فإن نهج التنقية مع إعادة التسمية هو الوحيد الذي يُصرَّف دائمًا. وتحصل المفاتيح غير المعرِّفة مثل first-name أو مفتاح يبدأ برقم مثل 2fa على المعاملة نفسها: اسم Rust صالح وإعادة تسمية إلى مفتاح JSON الحرفي.
فك التسلسل باستخدام serde_json
بمجرد أن تصير لديك البنية، أضِف الاعتماديات إلى Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
بعدها يكفي سطر واحد للتحليل. هذا المثال كامل ويُصرَّف كما هو:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub id: i64,
pub name: String,
pub email: String,
pub active: bool,
pub roles: Vec<String>,
}
fn main() -> Result<(), serde_json::Error> {
let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
let root: Root = serde_json::from_str(data)?;
println!("{} has {} role(s)", root.name, root.roles.len());
Ok(())
}
يقوم اشتقاق Deserialize بكل العمل. استخدم serde_json::from_str لنص، وfrom_slice لشريحة بايتات، وfrom_reader لملف أو جسم طلب HTTP، وserde_json::to_string لتحويل قيمة إلى JSON مجددًا. هذا هو العائد على إصابة الأنواع الصحيحة: فإن serde derive الذي ولّدته هو مُتحقِّق وقت تشغيل، لذا فإن استدعاء فكّ التسلسل عبر serde_json (serde_json deserialize) الذي يعيد Ok هو ضمان بأن البيانات طابقت بنيتك، لا مجرد اعتقاد من المُصرِّف بأنها فعلت.
التواريخ والمفاتيح الديناميكية والحقول المجهولة
بعض الأشكال لا تُطابَق حقلَ بنية بسيطًا، فيمنحها المولّد قيمة افتراضية معقولة تُحكِمها أنت لاحقًا يدويًا.
التواريخ. لا يملك JSON نوع تاريخ، لذا تصل طابعة زمنية بصيغة ISO أو RFC 3339 كنص String عادي. للتعامل الحقيقي مع التواريخ، حوّل الحقل إلى نوع من chrono وفعّل ميزة serde في chrono (chrono = { version = "0.4", features = ["serde"] }). عندئذٍ يحلّل serde صيغة RFC 3339 تلقائيًا:
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub name: String,
pub created_at: DateTime<Utc>,
}
المفاتيح الديناميكية. حين تتغيّر المفاتيح وقت التشغيل، مثل خريطة من المعرّفات إلى القيم، تكون البنية الثابتة شكلًا خاطئًا. استخدم HashMap مفتاحه String:
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
pub scores: HashMap<String, i64>,
}
الحقول الإضافية المجهولة. للحفاظ على بنية محدَّدة الأنواع مع التقاط المفاتيح التي لم تُنمذِجها، أضِف حقل #[serde(flatten)] مدعومًا بـ HashMap. أما القيمة الديناميكية حقًا فـ serde_json::Value هو الملتقِط الشامل لها. حين تكون حاجتك الفعلية التحقق من البنية بدلًا من توليد الأنواع، يغطّي دليل التحقق من JSON بـ JSON Schema فرض العقد من البداية إلى النهاية.
json-to-rust مقابل quicktype مقابل typeshare مقابل الطريقة اليدوية
لا توجد طريقة واحدة أفضل لإنتاج أنواع Rust من JSON. يعتمد الأمر على مكان وجود الـ JSON والاتجاه الذي تحوّل نحوه.
| النهج | الأنسب لـ | ملاحظة |
|---|---|---|
| محوّل عبر الإنترنت (هذه الأداة، transform.tools) | التحويلات لمرة واحدة، الحمولات الحساسة، صفر تثبيت | يستنتج من عيّنة، بالكامل من جانب العميل |
| quicktype | المخرجات متعددة اللغات، توليد الشفرة ضمن خط أنابيب | مبني أيضًا على عيّنة؛ عبر CLI أو الويب |
| typeshare | مشاركة أنواع Rust القائمة مع TS وSwift وKotlin | الاتجاه المعاكس: من Rust إلى لغات أخرى |
| schemafy / typify | التوليد من مخطط JSON | المدخل مخطط، لا عيّنة |
| كتابة البنى يدويًا | الحمولات الصغيرة جدًا، تعلّم serde | تحكّم كامل، لكنه ممل وعرضة للخطأ على نطاق واسع |
التمييز الجدير باستيعابه: المحوّل عبر الإنترنت وquicktype كلاهما يستنتج الأنواع من عيّنة JSON، بينما يعمل typeshare في الاتجاه المعاكس، محوّلًا أنواع Rust المُوسَّمة إلى TypeScript أو Swift. أما schemafy وtypify فيأخذان مخطط JSON Schema مدخلًا، لا بيانات مثال. إذا كانت قاعدة شفرتك TypeScript بدل Rust، فالنهج نفسه المبني على العيّنة ينطبق مع محوّل JSON إلى TypeScript، ويغطّي دليل واجهات JSON إلى TypeScript ذلك الجانب بعمق.
مزالق شائعة عند توليد Rust من JSON
البنى المولّدة نقطة بداية. انتبه لهذه قبل أن تثق بالمخرجات على بيانات حيّة.
- عيّنة واحدة نادرًا ما تكشف كل شكل. لا يمكن استنتاج الحقول الاختيارية إلا من عناصر مصفوفة متعددة. الصق مصفوفة تمثيلية كي يكون استنتاج Option دقيقًا لا تخمينًا من كائن واحد محظوظ.
- المصفوفات الفارغة أو المختلطة تتراجع إلى
serde_json::Value. هذه هي الإجابة الصادقة حين لا يوجد ما يُستنتَج منه. قدّم عيّنة أغنى للحصول على نوع عنصر محدَّد. - لا تُضيّق معرّفًا كبيرًا إلى i32. معرّفات snowflake وما يشبهها تتجاوز 2^53 وتفيض حقلًا من 32 بت. أبقِ i64 أو u64 المولّد.
- الحقل الذي يكون دائمًا null يصبح
Option<serde_json::Value>، لا اختياريًا عاديًا. لا يملك المحوّل نوعًا يستنتجه، فعامله كعنصر نائب وامنحه نوعًا حقيقيًا حالما تعرفه. serde_json::Valueيحتاج إلى اعتمادية serde_json. إذا احتوت المخرجات على Value وكانCargo.tomlلديك يفتقر إلى الحزمة، فلن تُصرَّف الشفرة.- التاريخ المتروك كـ String لن يُجري حسابات تواريخ. إذا كنت تنوي مقارنة الطوابع الزمنية أو تنسيقها، فتحوّل إلى نوع من chrono بدلًا من تحليل النصوص يدويًا.
أسئلة شائعة
لماذا يرفض serde الـ JSON لديّ رغم أن البنية تبدو صحيحة؟
يكون السبب دائمًا تقريبًا عدم تطابق في النوع. السببان المعتادان هما قيمة عشرية تحلّ في حقل i64، ومفتاح يغيب أحيانًا لكنه غير محدَّد النوع بـ Option. يفرض serde النوع عند فك التسلسل، لذا صحّح الحقل إلى f64 أو Option<T>، أو أعد توليد البنية من عيّنة تمثيلية.
أيّ نوع رقمي في Rust ينبغي أن يصير إليه عدد صحيح في JSON؟
الافتراضي هو i64. رقِّ إلى u64 حين تتجاوز القيمة i64::MAX، وتراجَع إلى f64 بعد u64::MAX. أي رقم مكتوب بفاصلة عشرية أو أُس يُطابَق f64 بغضّ النظر عن الحجم، لأن serde يرفض قيمة عشرية تُفكّ إلى حقل عدد صحيح.
كيف أجعل حقلًا اختياريًا حين يحذفه JSON؟
حدِّد نوعه بـ Option<T>. يعامل serde Option على أنه اختياري تلقائيًا ويفكّ المفتاح الغائب إلى None، لذا لا تحتاج إلى #[serde(default)]. يُعلّم المحوّل الحقل بـ Option حين تفتقر بعض العناصر المعايَنة إلى المفتاح.
هل أستخدم #[serde(rename)] أم #[serde(rename_all)]؟
استخدم #[serde(rename)] لكل حقل حين تخلط الحمولة أنماط التسمية؛ فهو يعمل دائمًا. إذا اتّبع كل حقل في البنية اصطلاحًا واحدًا، فاحذف الأوسمة المخصّصة لكل حقل وضع بدلًا منها #[serde(rename_all = "camelCase")] واحدًا على البنية. كلاهما يفكّ التسلسل بالطريقة نفسها.
كيف أعالج مفاتيح JSON التي هي كلمات Rust محجوزة مثل type؟
يُصدر المولّد type_ مع #[serde(rename = "type")] يُعيد الربط بالمفتاح الأصلي. هذا أمتن من معرّف خام مثل r#type، لأن المعرّفات الخام لا تستطيع تغطية self وcrate وsuper في كل موضع.
لماذا تُحدَّد أنواع تواريخ JSON بـ String بدلًا من نوع تاريخ؟
لا يملك JSON نوع تاريخ، فالطابعة الزمنية مجرد نص على الشبكة، وString هو الافتراضي الصادق. للتعامل الحقيقي مع التواريخ، غيّر الحقل إلى DateTime<Utc> أو NaiveDate من chrono وفعّل ميزة serde في chrono؛ عندئذٍ يحلّل serde صيغة RFC 3339 نيابةً عنك.
كيف أفكّ تسلسل JSON إلى البنية المولّدة؟
أضِف serde_json إلى Cargo.toml، ثم اكتب let root: Root = serde_json::from_str(json)?;. يتولّى اشتقاق Deserialize الباقي. استخدم from_slice لشريحة بايتات وfrom_reader لملف أو جسم طلب HTTP.
هل بيانات JSON الخاصة بي خاصة عند استخدام محوّل JSON إلى بنية Rust عبر الإنترنت؟
نعم. يجري التحويل بنسبة 100% في متصفحك باستخدام JavaScript. لا يغادر الـ JSON لديك الصفحةَ أبدًا ولا يُرسَل إلى خادم، بما في ذلك الرموز والمعرّفات وبيانات العملاء. حين تكون جاهزًا، جرّب محوّل JSON إلى Rust على حمولتك الخاصة.