الشبكات وHTTP

التعامل مع JSON

20 دقيقة الدرس 8 من 13

التعامل مع JSON

JSON هو اللغة المشتركة للواجهات البرمجية الحديثة. كل عميل REST تكتبه سيحلّل استجابات JSON ويُسلسل كائنات Java إلى أجسام طلبات JSON. في هذا الدرس ستتعلّم كيفية القيام بذلك باحترافية باستخدام Jackson — المكتبة القياسية الفعلية لـ JSON في عالم Java — إلى جانب لمحة عن Gson حتى تتمكّن من تقييم المقايضات واتخاذ قرارات مدروسة.

لماذا Jackson؟

Jackson هو محرك JSON الافتراضي في Spring Boot وQuarkus وMicronaut وعمليًا كل إطار عمل Java رئيسي آخر. إنه مُختبَر في بيئات الإنتاج، سريع للغاية، وغني بالميزات. الحزمة الأساسية هي jackson-databind التي تستدعي تلقائيًا jackson-core وjackson-annotations:

<!-- Maven --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.1</version> </dependency>
Jackson مقابل Gson مقابل org.json: Gson (من Google) أبسط للتضمين في مشاريع صغيرة بلا إطار عمل. org.json خيار خفيف بلا تبعيات للتحليل العرضي لكنه يخلو من ربط البيانات. Jackson يمتلك أغنى مجموعة ميزات وأفضل أداء على نطاق واسع — فضّله في أي خدمة إنتاج.

ObjectMapper — مثيل واحد، آمن للخيوط

ObjectMapper هو القلب النابض لـ Jackson. إنشاؤه مكلف وهو آمن للخيوط بعد الضبط، لذا أنشئ مثيلًا مشتركًا واحدًا (مثلًا كحقل static final أو singleton bean):

import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public final class JsonMapper { // مثيل مشترك واحد — آمن للاستخدام المتزامن public static final ObjectMapper MAPPER = new ObjectMapper() .registerModule(new JavaTimeModule()) // دعم Instant وLocalDate وما شابه .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); private JsonMapper() {} }
لا تُنشئ ObjectMapper جديدًا في كل طلب. كل إنشاء يحمّل بيانات Reflection لكل فئة يصادفها، مما يجعله أبطأ بأوامر من حجم مقارنةً بإعادة استخدام مثيل مضبوط.

إلغاء تسلسل JSON إلى كائنات Java (القراءة)

بالنظر إلى نص JSON من جسم استجابة HTTP، تُعيّن ObjectMapper.readValue() البيانات إلى كائن Java مُحدَّد النوع. أولًا عرّف الفئة الهدف باستخدام Java record (المفضّل في Java 17+) أو POJO مع getters:

// سجل المجال — تُعيَّن الحقول إلى مفاتيح JSON حسب الاسم public record Product( long id, String name, double price, int stock ) {}
import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; ObjectMapper mapper = JsonMapper.MAPPER; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products/42")) .header("Accept", "application/json") .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // إلغاء تسلسل جسم الاستجابة إلى Product مُحدَّد النوع Product product = mapper.readValue(response.body(), Product.class); System.out.printf("Product: %s, Price: %.2f%n", product.name(), product.price());

التعامل مع مصفوفات JSON والمجموعات العامة

حين تُعيد الواجهة البرمجية مصفوفة JSON، تحتاج إلى TypeReference للحفاظ على النوع العام في وقت التشغيل (Java تمسح الأنواع العامة على مستوى البايت كود، لذا List<Product>.class تعبير غير صالح):

import com.fasterxml.jackson.core.type.TypeReference; import java.util.List; // جسم الاستجابة: [{"id":1,"name":"Keyboard",...}, {"id":2,"name":"Mouse",...}] List<Product> products = mapper.readValue( response.body(), new TypeReference<List<Product>>() {} ); products.forEach(p -> System.out.println(p.name()));
استخدم TypeReference لأي نوع عام. يعمل بنفس الكفاءة مع Map<String, Object> أو List<Map<String, String>> أو أي نوع مُحدَّد بمعاملات آخر. صيغة الفئة المجهولة {} في النهاية مقصودة — فهي تلتقط توقيع النوع العام في البايت كود ليتمكّن Jackson من قراءته عبر Reflection.

تسلسل كائنات Java إلى JSON (الكتابة)

العملية العكسية — تحويل كائن Java إلى نص JSON لجسم طلب — تستخدم writeValueAsString():

public record NewProduct(String name, double price, int stock) {} // بناء كائن Java NewProduct payload = new NewProduct("Wireless Keyboard", 49.99, 200); // التسلسل إلى نص JSON String json = mapper.writeValueAsString(payload); // {"name":"Wireless Keyboard","price":49.99,"stock":200} HttpRequest postRequest = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com/products")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build();

التحكّم في التعيين بالتعليقات التوضيحية

نادرًا ما تتطابق مفاتيح JSON في الواجهات البرمجية الحقيقية مع أعراف تسمية Java تمامًا. يوفّر Jackson تعليقات توضيحية لسدّ هذه الهوّة:

import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; @JsonInclude(Include.NON_NULL) // يحذف الحقول الفارغة (null) من الإخراج المُسلسَل public record UserProfile( @JsonProperty("user_id") long id, // يُعيَّن من/إلى "user_id" في JSON @JsonProperty("full_name") String fullName, String email, @JsonIgnore String passwordHash // لا يُسلسَل ولا يُلغى تسلسله أبدًا ) {}
  • @JsonProperty("key") — يُعيد تسمية الحقل في JSON. ضروري حين تستخدم الواجهة البرمجية snake_case وتستخدم Java camelCase.
  • @JsonIgnore — يستبعد الحقل من التسلسل وإلغاء التسلسل معًا. استخدمه للبيانات الحساسة كبصمات كلمات المرور.
  • @JsonInclude(NON_NULL) — يحذف حقول null من الإخراج للحفاظ على إيجاز JSON.
  • @JsonAlias — يقبل أسماء متعددة أثناء إلغاء التسلسل (مفيد حين تغيّر واجهة برمجية اسم حقل وتحتاج لدعم الإصدارين أثناء مرحلة الانتقال).

استراتيجية تسمية Snake-Case العالمية

بدلًا من تعليق توضيحي على كل حقل، يمكنك ضبط ObjectMapper عالميًا للترجمة التلقائية بين camelCase (Java) وsnake_case (الواجهة البرمجية):

import com.fasterxml.jackson.databind.PropertyNamingStrategies; ObjectMapper mapper = new ObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); // الآن "fullName" في Java <-> "full_name" في JSON تلقائيًا

قراءة JSON التعسّفي بـ JsonNode

في بعض الأحيان لا تملك — أو لا تريد — مخططًا ثابتًا. تمنحك JsonNode نموذجًا شجريًا ديناميكيًا، شبيهًا بـ DOM لكن لـ JSON. هذا ذو قيمة لفحص استجابات الواجهات البرمجية أثناء التطوير أو لمعالجة بيانات غير متجانسة:

import com.fasterxml.jackson.databind.JsonNode; JsonNode root = mapper.readTree(response.body()); // التنقّل في الشجرة String name = root.get("name").asText(); double price = root.get("price").asDouble(); boolean inStock = root.path("meta").path("in_stock").asBoolean(false); // path() لا ترفع استثناء // التكرار على عقدة مصفوفة JSON JsonNode tags = root.get("tags"); if (tags != null && tags.isArray()) { for (JsonNode tag : tags) { System.out.println(tag.asText()); } }
فضّل path() على get() عند التنقّل في الحقول الاختيارية. تُعيد get() قيمة null للمفتاح المفقود مما سيتسبّب في NullPointerException عند الاستدعاء التالي. أما path() فتُعيد عقدة MissingNode آمنة يمكن الاستمرار في التنقّل منها وتُنتج قيمة افتراضية عند التحويل بـ asText() أو asInt() وما شابه.

إنتاج JSON بـ ObjectNode

حين تحتاج لبناء حمولة JSON ديناميكيًا (حقول تُحدَّد في وقت التشغيل)، استخدم ObjectNode بدلًا من تسلسل النصوص:

import com.fasterxml.jackson.databind.node.ObjectNode; ObjectNode body = mapper.createObjectNode(); body.put("event", "page_view"); body.put("user_id", 9001); body.put("timestamp", System.currentTimeMillis()); ObjectNode props = body.putObject("properties"); props.put("page", "/checkout"); props.put("referrer", "https://google.com"); String json = mapper.writeValueAsString(body); // {"event":"page_view","user_id":9001,"timestamp":...,"properties":{"page":"/checkout","referrer":"..."}}

التعامل مع الحقول غير المعروفة بمرونة

تتطوّر الواجهات البرمجية وأحيانًا تُعيد حقولًا لا يُنمذجها كودك Java. بشكل افتراضي يرفع Jackson استثناء UnrecognizedPropertyException للحقول غير المعروفة. في كود الإنتاج، اضبط الـ mapper لتجاهلها حتى لا يتعطّل عميلك بسبب إضافات بريئة في الواجهة البرمجية:

import com.fasterxml.jackson.databind.DeserializationFeature; ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
هذا الخيار سلاح ذو حدّين. تجاهل الخصائص غير المعروفة يحميك من الأعطال، لكنه يعني أيضًا أن الأخطاء المطبعية في أسماء الحقول أثناء التطوير تُنتج حقولًا فارغة/افتراضية صامتة عوضًا عن خطأ واضح. احتفظ بالسلوك الصارم الافتراضي أثناء التطوير وانتقل إلى السلوك المرن في ضبط الإنتاج فحسب.

Jackson مقابل Gson — متى تختار أيًّا منهما

  • Jackson — الخيار الأمثل لخدمات الإنتاج وتكامل الأطر والبث الجزئي للحمولات الضخمة والتحكّم الغني عبر التعليقات التوضيحية.
  • Gson — واجهة برمجية أبسط، عمل بلا ضبط للاستخدام الأساسي، جيد لـ Android أو الأدوات الصغيرة حيث لا يمكنك استخدام Jackson الكامل.
  • org.json — بلا تبعيات، جيد للنصوص البرمجية أو الأدوات حيث إضافة مكتبة ليست مقبولة. لا يحتوي على ربط بيانات؛ تصل إلى الحقول يدويًا بالاسم.

الخلاصة

لديك الآن مجموعة أدوات متكاملة لـ JSON في Java: أنشئ ObjectMapper مشتركًا واحدًا، ألغِ التسلسل من الاستجابات بـ readValue() وTypeReference، سلسِل الحمولات بـ writeValueAsString()، تحكّم في التعيين بالتعليقات التوضيحية، تنقّل في البنى غير المعروفة بـ JsonNode، وابنِ حمولات ديناميكية بـ ObjectNode. هذه الأنماط تغطّي الغالبية العظمى من عمل JSON في عملاء REST الحقيقيين. الدرس التالي يربط كل شيء معًا: بناء عميل REST متكامل وجاهز للإنتاج.