آموزشدانستنی‌ها

JSON با Jackson، (بخش سوم) Tree Model

نوشته‌ی حاضر به Tree Model می‌پردازد که یکی از سه روش تولید و خواندن JSON با بسته‌ی‌ نرم‌افزاری Jackson است. همانند مقاله‌ی قبلی مثالی را با این روش بررسی کرده و در انتها به این سوال می‌پردازیم: «در چه مواردی این متد را بر دو رقیب دیگر ترجیح دهیم؟»

در مقاله‌ی اول پس از معرفی بسته‌ی نرم‌افزاری Jackson یک مثال با روش Data Binding بررسی کردیم و در مقاله‌ی دوم روش Stream Method معرفی شد، سومین روش یا Tree Model در ادامه شرح داده می‌شود.

تولید JSON در Tree Model

روش کار در Tree Model به این صورت است که ابتدا یک شی از ObjectNode به عنوان «شی ریشه» یا «root object» ساخته می‌شود. شی ریشه معادل کلاس اصلی در JSON است. در ادامه مقادیر key/value را به ریشه ملحق می‌کنیم. برای این کار از نسخه‌های مختلف متد put یا set از کلاس ObjectNode استفاده خواهیم کرد. برای حاصل کردن objectNode دو روش وجود دارد:

  • روش اول: ساخت یک شی ObjectMapper و استفاده از متد createObjectNode آن.
com.fasterxml.jackson.databind.node.ObjectNode createObjectNode()
  • روش دوم: ساخت یک شی JsonNodeFactory و استفاده از متد objectNode آن.
com.fasterxml.jackson.databind.node.ObjectNode objectNode()

در تکه کد زیر از روش دوم استفاده شده است.

private static JsonNode createRootJsonNode() {
	//create root node
	JsonNodeFactory jsonNodeFactory = new JsonNodeFactory(false); //false for bigDecimalExact
	ObjectNode rootObjectNode = jsonNodeFactory.objectNode();
	Employee emp = JacksonDemo0.createEmployee();
	//id
	rootObjectNode.put("id", emp.getId());
	//names
	rootObjectNode.put("firstName", emp.getFirstName());
	rootObjectNode.put("lastName", emp.getLastName());
	//phone numbers		
	ArrayNode phoneNumbersarrayNode = jsonNodeFactory.arrayNode();
	for (String phoneNumber : emp.getPhoneNumbers()) {
		phoneNumbersarrayNode.add(phoneNumber);
	}
	rootObjectNode.set("phoneNumbers", phoneNumbersarrayNode);
	//address
	ObjectNode addressObjectNode = jsonNodeFactory.objectNode();
	addressObjectNode.put("city", emp.getAddress().getCity());
	addressObjectNode.put("street", emp.getAddress().getStreet());
	addressObjectNode.put("zipCode", emp.getAddress().getZipCode());
	rootObjectNode.set("address", addressObjectNode);
	
	return rootObjectNode;
}

هنگام ساخت شیء‌ریشه، با استفاده از متد‌های زیر key/Value های JSON را ساخته و به آن اضافه کردیم. value می‌تواند مقادیر ساده (رشته یا عدد) یا مقادیر مرکب (مانند شی JSON یا آرایه) باشد. از لیست زیر دو متد set و setAll برای مواردی است که نوع value یکی از موارد مرکب باشد. در این صورت ابتدا شیء‌ای از ObjectNode یا ArrayNode را ساخته و سپس روی شیء ریشه، الصاق می‌کنیم.

ObjectNode put(String fieldName, !!VariableType!! v)

JsonNode set(String fieldName, JsonNode value)
JsonNode setAll(Map<String,? extends JsonNode> properties)

VariableType در شبه کد فوق می‌تواند یکی از موارد زیر باشد:

BigDecimal, BigInteger, boolean, Boolean, byte[], double, float, int, long, short, String

شکل زیر ساختار کلاس‌های API و نسبت JsonNode را با ObjectNode و ArrayNode نشان می‌دهد.

تکه کد زیر نشان می‌دهد پس از ساختن «شیء‌ریشه»، نوشتن متن JSON ای تولید شده در یک فایل چگونه انجام می‌شود.

private static void emp2JSON_TreeModel() throws IOException, JsonProcessingException {
	JsonNode rootJsonNode = createRootJsonNode();
	
	JsonFactory jsonFactory = new JsonFactory();
	JsonGenerator jsonGenerator = jsonFactory.createGenerator(new File("treeMethodDB.json"), JsonEncoding.UTF8);
	
	ObjectMapper objectMapper = new ObjectMapper();
	objectMapper.writeTree(jsonGenerator, rootJsonNode);
}

از یکی از متد‌های زیر، که از کلاس ObjectMapper هستند، استفاده کرده‌ایم:

void writeTree(JsonGenerator jgen, JsonNode rootNode)
void writeTree(JsonGenerator jgen, TreeNode rootNode)

اجازه دهید از تفاوت‌های دو متد عبور کنیم و تنها به این نکته اشاره‌ داشته باشیم که TreeNode interface توسط JsonNode پیاده‌سازی شده و تفاوت فاحشی بین دو متد بالا نیست.

JsonGenerator نیز، کلاس آشنایی است. چرا که در مقاله‌ی قبل برای تولید JSON با Stream Method از آن استفاده کردیم. به وسیله‌ی این کلاس مشخص می‌کنیم داده‌ای که تولید می‌کنیم «کجا» و با چه «Encoding»‌ای نوشته شود.

خواندن JSON در Tree Model

همان‌طور که در تولید JSON به روش Tree Model بخش زیادی از کار مربوط به ساختن «شیء ریشه» بود، در «خواندن» به این روش نیز، تمرکز کار روی به‌دست آوردن یک شیء ریشه از نوع JsonNode و سپس پیمایش (traverse) آن است.
برای به دست آوردن root JsonNode دو روش وجود دارد:

  • روش اول: استفاده از یکی از نسخه های overload شده‌ی متد زیر از کلاس ObjectMapper که می‌تواند داده‌ی JSON را از یکی از منابع بخواند و یک شیء از نوع JsonNode برگرداند.
JsonNode readTree(!!SourceType!! source)

SourceType در شبه کد فوق می‌تواند یکی از موارد زیر باشد:

byte[] , File, InputStream, JsonParser, Reader, String, URL

  • روش دوم: استفاده از یکی از نسخه‌های overload شده‌ی متد زیر از کلاس ObjectMapper که پارامتر اول آن، نوع منبع را معرفی کرده و پارامتر دوم از نوع Class است. با تنظیم پارامتر دوم به JsonNode.class خروجی مطلوب حاصل می‌شود.
T readValue(!!SourceType!! src, Class<T> valueType)

SourceType در شبه کد فوق می‌تواند یکی از موارد زیر باشد:

byte[] , File, InputStream, JsonParser, Reader, String, URL  AND DataInput

در مثال زیر با استفاده از روش اول یک root JsonNode را از یک فایل به دست می‌آوریم:

private static JsonNode retrieveRootJsonNode() throws JsonProcessingException, IOException {
	ObjectMapper objectMapper = new ObjectMapper();
	JsonNode rootJsonNode = objectMapper.readTree(new File("treeMethodDB.json"));
	return rootJsonNode;
}

حال باید بتوانیم به فیلد‌های شیء‌ریشه دسترسی پیدا کنیم. متد‌های زیر در JsonNode به این منظور ارائه شده‌اند.

توضیح متد
برای آرایه‌ها، المان موجود در index را برمی‌گرداند. شروع شمارش از صفر است. اگر المان با شماره‌ی index در دسترس نباشد یکی از موارد زیر اتفاق می‌افتد:

  • اگر index خارج از محدوده‌ی مناسب برای آرایه باشد (یعنی کوچکتر از صفر یا بزرگتر از اندازه ی آرایه باشد)، null برمی‌گرداند.
  • اگر value مربوط به این فیلد از نوع ArrayNode نباشد، null برمی‌گرداند.
  • اگر value مربوط به این فیلد از نوع null باشد، مقداری که برگردانده می‌شود از نوع NullNode خواهد بود، نه از نوع null جاوا.
JsonNode get(int index)
برای Object ها مقدار فیلدی که نام آن property باشد را بر می‌گرداند. اگر :

  • فیلدی با این نام وجود نداشته باشد null برمی‌گرداند.
  • اگر value مربوط به این فیلد از نوع ObjectNode نباشد، null بر می‌گرداند.
  • اگر value مربوط به این فیلد از نوع null باشد، مقداری که برگردانده می‌شود از نوع NullNode خواهد بود. نه از نوع null جاوا.
JsonNode get(String property)
مشابه متد‌های get در ردیف‌های فوق، با این تفاوت که به جای null جاوا در موارد گفته شده، شیء‌ای از نوع MissingNode بر‌می‌گرداند. JsonNode path(int index) JsonNode path(String property)
  • در صورتی که این متد روی فیلدی اجرا شود که value آن از نوع Object باشد JsonNode حاوی آن Object را برمی‌گرداند.
  • در صورتی که فیلدی به این نام وجود نداشته باشد، فیلدی از نوع Object ساخته می‌شود، به شیء اضافه می‌شود و سپس برگردانده می‌شود.
  • در صورتی که این متد روی فیلدی اجرا شود که value آن از نوع Object نباشد UnsupportedOperationException پرتاب می‌شود.
JsonNode with(String propertyName)
  • در صورتی که این متد روی فیلدی اجرا شود که value آن از نوع Array باشد آن را برمی‌گرداند.
  • در صورتی که فیلدی به این نام وجود نداشته باشد، فیلدی از نوع Array ساخته می‌شود، به شیء اضافه می‌شود و سپس برگردانده می‌شود.
  • در صورتی که این متد روی فیلدی اجرا شود که value آن از نوع Array نباشد UnsupportedOperationException پرتاب می‌شود.
JsonNode withArray(String propertyName)

پس از اینکه فیلد را در قالب JsonNode به دست آوردیم، می‌توانیم از متد‌های زیر استفاده کنیم تا مقدار value آن را مشاهده کنیم.

  • متد‌های سری XXXvalue : (مانند intValue) این متد‌ها برای استخراج مقدار به کار می‌روند، در مواردی که نوع آن را به طور دقیق می‌دانیم.
  • متدهای سری asXXX: (مانند asInt) این متد‌ها برای تبدیل و استخراج مقدار به کار می‌روند. صرف نظر از نوع ذخیره‌ی داده در JSON آن را به نوع دلخواه ما تغییر می‌دهد (اگر تغییر ممکن باشد، مانند تبدیل قابل قبول int به String).

در مثال زیر از متد‌هایی که توضیح داده شد، برای استخراج داده از JSON و ساختن یک Employee استفاده شده است.

private static Employee json2Employee_TreeModel() throws JsonProcessingException, IOException {
	Employee emp = new Employee();
	JsonNode rootJsonNode = retrieveRootJsonNode();
	
	//id
	emp.setId(rootJsonNode.get("id").intValue());
	//names
	emp.setFirstName(rootJsonNode.get("firstName").textValue());
	emp.setLastName(rootJsonNode.get("lastName").textValue());
	//phoneNumbers
	ArrayList<String> phoneNumbers = new ArrayList<>();
	for (JsonNode phoneNumberNode : rootJsonNode.get("phoneNumbers")) {
		phoneNumbers.add(phoneNumberNode.textValue());
	}
	emp.setPhoneNumbers(phoneNumbers);
	
	//address
	Address address = new Address();
	address.setCity(rootJsonNode.get("address").get("city").textValue());
	address.setStreet(rootJsonNode.get("address").get("street").textValue());
	address.setZipCode(rootJsonNode.get("address").get("zipCode").intValue());
	emp.setAddress(address);
	return emp;
}

پیمایش JSON در Tree Model

در مثال فوق بخش مهمی از متد‌های Tree Model را برای به دست آوردن فیلد‌های JSON دیدیم. استفاده از تمام متد‌های فوق مستلزم دانستن نام فیلد داده هستند. اما اگر یک داده‌ی JSON ای در اختیار ما قرار گیرد و در مورد ساختار و فیلد‌های آن هیچ اطلاعی نداشته باشیم چگونه باید آن را پیمایش کرده و اطلاعات مورد نیاز را استخراج کنیم؟

Tree Model برای چنین مواردی متدهایی را در اختیار قرار داده است. از جمله:

Iterator<String> fieldNames()
Iterator<Map.Entry<String,JsonNode>> fields()
Iterator<JsonNode> elements()
Iterator<JsonNode> iterator()
abstract JsonNodeType getNodeType()
boolean isXXX()

به عنوان مثال اگر شکل JSON ای که در قسمت‌های قبلی برای یک شی از کلاس Employee ساخته‌ایم را دریافت کرده و شیء‌ریشه‌ی آن را به عنوان ورودی به متد زیر بدهیم می‌توانیم فیلد‌های آن را پیمایش کرده و در مورد هر فیلد نام و نوع value را به دست آورده و چاپ کنیم.

private static void printNameOfFields(JsonNode root) {
	Iterator<String> fNameIterator = root.fieldNames();
	while (fNameIterator.hasNext()) {
		System.out.println(fieldName+": "+root.get(fieldName).getNodeType());
	}
}

خروجی اجرای تکه کد فوق به این صورت است:

id: NUMBER
firstName: STRING
lastName: STRING
phoneNumbers: ARRAY
address: OBJECT

ویژگی Tree Model

عمده‌ی استفاده این روش زمانی است که بین کلاس‌های پروژه یک POJO معادل JSON نداریم، چرا که اگر یک کلاس معادل JSON وجود داشته باشد بهترین و به صرفه‌ترین متد استخراج داده از آن، استفاده از Data Binding است. در مواردی که داده‌ای در اختیارمان قرار‌گرفته و در مورد محتوا، نام فیلد‌ها، نوع Value فیلد‌ها و… هیچ اطلاعی نداریم، متد‌های پیمایش Tree Model راهگشا‌ خواهند بود.

این روش، آشناترین شکل از داده برای برنامه‌نویسانی است که با فرمت XML کار کرده‌اند، چرا که در سطح مفهومی، ساختار درختیِ اشیاء JSON بسیار مشابه مفهوم DOM در فضای XML است.

بخش چهارم-الف

.

.

.

با ما همراه باشید

آدرس کانال تلگرام: JavaCupIR@

آدرس اکانت توییتر: JavaCupIR@

آدرس صفحه اینستاگرام: javacup.ir

آدرس گروه لینکدین: Iranian Java Developers

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا