Go: إلغاء تسلسل JSON باستخدام كتابة غير صحيحة ، أو كيفية التغلب على أخطاء مطور واجهة برمجة التطبيقات

صورة

في الآونة الأخيرة ، حدث أن قمت بتطوير عميل http على Go لخدمة توفر REST API مع json كتنسيق ترميز. مهمة قياسية ، ولكن أثناء العمل كان علي مواجهة مشكلة غير قياسية. أقول لك ما هي النقطة.

كما تعلم ، يحتوي تنسيق json على أنواع بيانات. أربعة بدائية: سلسلة ، عدد ، منطقية ، خالية ؛ ونوعان هيكليان: كائن وصفيف. في هذه الحالة ، نحن مهتمون بالأنواع البدائية. فيما يلي مثال لرمز json مع أربعة حقول من أنواع مختلفة:

{
	"name":"qwerty",
	"price":258.25,
	"active":true,
	"description":null,
}

كما يوضح المثال ، يتم إحاطة قيمة السلسلة بعلامات اقتباس. رقمي - لا يحتوي على علامات اقتباس. يمكن أن يحتوي النوع المنطقي على قيمة واحدة فقط من قيمتين: صواب أو خطأ (بدون علامات اقتباس). وبالتالي يكون النوع الفارغ فارغًا (أيضًا بدون علامات اقتباس).

والآن المشكلة نفسها. في مرحلة ما ، في فحص مفصل لرمز json الذي تم تلقيه من خدمة طرف ثالث ، وجدت أن أحد الحقول (دعنا نسميه سعرًا) يحتوي بشكل دوري على قيمة سلسلة (الرقم في علامات الاقتباس) بالإضافة إلى القيمة العددية. بمعنى ، يمكن أن يُرجع نفس الاستعلام بمعلمات مختلفة رقمًا كرقم ، أو يمكنه إرجاع نفس الرقم كسلسلة. لا يمكنني أن أتخيل كيف يتم تنظيم الشفرة التي تُرجع مثل هذه النتائج في الطرف الآخر ، ولكن يبدو أن هذا يرجع إلى حقيقة أن الخدمة نفسها هي مُجمِّع وتسحب البيانات من مصادر مختلفة ، ولم يقدم المطورون استجابة الخادم json إلى تنسيق واحد. ومع ذلك ، من الضروري العمل مع ما هو.

ولكن بعد ذلك فوجئت أكثر. الحقل المنطقي (دعنا نسميه نشطًا) ، بالإضافة إلى صواب وخطأ ، أعاد قيم السلسلة "صواب" و "خطأ" وحتى رقمي 1 و 0 (صواب وخطأ ، على التوالي).

كل هذا الارتباك حول أنواع البيانات لن يكون حرجًا إذا كنت سأعالج json say في PHP المكتوب بشكل ضعيف ، ولكن Go لديه كتابة قوية ، ويتطلب إشارة واضحة إلى نوع الحقل الذي تم إلغاء تسلسله. ونتيجة لذلك ، كانت هناك حاجة لتطبيق آلية تسمح بتحويل جميع قيم الحقل النشط إلى نوع منطقي أثناء عملية إلغاء التسلسل ، وأي قيمة لحقل السعر إلى قيمة رقمية.

لنبدأ بمجال السعر.

لنفترض أن لدينا كود json مثل هذا:

[
	{"id":1,"price":2.58},
	{"id":2,"price":7.15}
]

أي أن json يحتوي على مصفوفة من الكائنات ذات حقلين من النوع الرقمي. يبدو رمز إلغاء التسلسل القياسي لهذا json on Go كما يلي:

type Target struct {
	Id    int     `json:"id"`
	Price float64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":4,"price":7.15}]`

	targets := []Target{}

	err := json.Unmarshal([]byte(jsonString), &targets)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price)
	}
}

في هذا الكود ، سنقوم بإلغاء تسلسل حقل المعرف إلى int وحقل السعر إلى float64. لنفترض الآن أن رمز json الخاص بنا يبدو كما يلي:

[
	{"id":1,"price":2.58},
	{"id":2,"price":"2.58"},
	{"id":3,"price":7.15},
	{"id":4,"price":"7.15"}
]

أي أن حقل السعر يحتوي على قيم لكل من النوع الرقمي والسلسلة. في هذه الحالة ، يمكن فقط فك القيم الرقمية لحقل السعر إلى النوع float64 ، بينما تتسبب قيم السلسلة في حدوث خطأ حول عدم توافق الأنواع. هذا يعني أنه لا يوجد float64 ولا أي نوع بدائي آخر مناسب لإلغاء تسلسل هذا المجال ، ونحن بحاجة إلى نوع مخصص خاص بنا مع منطق إلغاء التسلسل الخاص به.

على هذا النحو ، قم بتعريف بنية CustomFloat64 بحقل Float64 واحد من النوع float64.

type CustomFloat64 struct{
	Float64 float64
}

ووضح على الفور هذا النوع لحقل السعر في الهيكل المستهدف:

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

تحتاج الآن إلى وصف منطقك الخاص لفك ترميز حقل من نوع CustomFloat64.

تحتوي حزمة "التشفير / json" على طريقتين خاصتين: MarshalJSON و UnmarshalJSON ، المصممة لتخصيص منطق التشفير وفك التشفير لنوع بيانات مستخدم معين. يكفي تجاوز هذه الأساليب ووصف التنفيذ الخاص بك.

تجاوز أسلوب UnmarshalJSON لنوع عشوائي CustomFloat64. في هذه الحالة ، من الضروري اتباع توقيع الطريقة بدقة ، وإلا فلن تعمل ببساطة ، والأهم من ذلك أنها لن تنتج خطأ.

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {

عند الإدخال ، تأخذ هذه الطريقة شريحة من البايت (البيانات) ، والتي تحتوي على قيمة حقل معين من json الذي تم فك ترميزه. إذا قمنا بتحويل هذا التسلسل من وحدات البايت إلى سلسلة ، فسوف نرى قيمة الحقل بالضبط في الشكل الذي كُتبت فيه في json. بمعنى ، إذا كان نوع سلسلة ، فسوف نرى بالضبط سلسلة بعلامات اقتباس مزدوجة ("258") ، إذا كانت من نوع رقمي ، فسوف نرى سلسلة بدون علامات اقتباس (258).

لتمييز قيمة رقمية من قيمة سلسلة ، يجب عليك التحقق مما إذا كان الحرف الأول هو علامة اقتباس. نظرًا لأن حرف الاقتباس المزدوج في جدول UNICODE يستهلك بايتًا واحدًا ، نحتاج فقط إلى التحقق من البايت الأول لشريحة البيانات من خلال مقارنته برقم الحرف في UNICODE. هذا هو الرقم 34. لاحظ أنه بشكل عام ، الحرف لا يعادل البايت ، حيث يمكن أن يستغرق أكثر من بايت واحد. الرمز في Go يكافئ رون (رون). في حالتنا ، هذا الشرط كافٍ:

if data[0] == 34 {

إذا تم استيفاء الشرط ، فإن القيمة لها نوع سلسلة ، ونحتاج إلى الحصول على السلسلة بين علامات الاقتباس ، أي شريحة الشريحة بين البايت الأول والأخير. تحتوي هذه الشريحة على قيمة عددية يمكن فك شفرتها في النوع البدائي float64. هذا يعني أنه يمكننا تطبيق طريقة json.Unmarshal عليها ، مع حفظ النتيجة في حقل Float64 لهيكل CustomFloat64.

err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)

إذا لم تبدأ شريحة البيانات بعلامة اقتباس ، فإنها تحتوي بالفعل على نوع بيانات رقمي ، ويمكننا تطبيق طريقة json.Unmarshal مباشرة على شريحة البيانات بالكامل.

err := json.Unmarshal(data, &cf.Float64)

إليك الشفرة الكاملة لطريقة UnmarshalJSON:

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == 34 {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

ونتيجة لذلك ، باستخدام طريقة json.Unmarshal إلى كود json الخاص بنا ، سيتم تحويل جميع قيم حقل السعر بشفافية إلى نوع بدائي float64 بالنسبة لنا ، وستتم كتابة النتيجة إلى حقل Float64 من بنية CustomFloat64.

قد نحتاج الآن إلى تحويل بنية Target إلى json. ولكن ، إذا طبقنا طريقة json.Marshal مباشرة على نوع CustomFloat64 ، فإننا نقوم بتسلسل هذه البنية ككائن. نحن بحاجة إلى ترميز حقل السعر في قيمة عددية. لتخصيص منطق الترميز من النوع المخصص CustomFloat64 ، نقوم بتطبيق طريقة MarshalJSON لذلك ، مع مراقبة توقيع الأسلوب بدقة:

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

كل ما عليك القيام به في هذه الطريقة هو مرة أخرى لاستخدام طريقة json.Marshal ، ولكن تطبيقها بالفعل ليس على هيكل CustomFloat64 ، ولكن على حقل Float64 الخاص به. من الطريقة نعيد شريحة البايت المستقبلة والخطأ.

إليك الشفرة الكاملة التي تعرض نتائج التسلسل وإلغاء التسلسل (يتم حذف تدقيق الأخطاء للإيجاز ، ويكون عدد البايت برمز علامة الاقتباس المزدوجة ثابتًا):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomFloat64 struct {
	Float64 float64
}

const QUOTES_BYTE = 34

func (cf *CustomFloat64) UnmarshalJSON(data []byte) error {
	if data[0] == QUOTES_BYTE {
		err := json.Unmarshal(data[1:len(data)-1], &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	} else {
		err := json.Unmarshal(data, &cf.Float64)
		if err != nil {
			return errors.New("CustomFloat64: UnmarshalJSON: " + err.Error())
		}
	}
	return nil
}

func (cf CustomFloat64) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cf.Float64)
	return json, err
}

type Target struct {
	Id    int           `json:"id"`
	Price CustomFloat64 `json:"price"`
}

func main() {
	jsonString := `[{"id":1,"price":2.58},
					{"id":2,"price":"2.58"},
					{"id":3,"price":7.15},
					{"id":4,"price":"7.15"}]`

	targets := []Target{}

	_ := json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Price.Float64)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

نتيجة تنفيذ التعليمات البرمجية:

1 - 2.58
2 - 2.58
3 - 7.15
4 - 7.15
[{"id":1,"price":2.58},{"id":2,"price":2.58},{"id":3,"price":7.15},{"id":4,"price":7.15}]

دعنا ننتقل إلى الجزء الثاني وننفذ نفس الرمز لإلغاء التسلسل json مع قيم غير متناسقة للحقل المنطقي.

لنفترض أن لدينا كود json مثل هذا:

[
	{"id":1,"active":true},
	{"id":2,"active":"true"},
	{"id":3,"active":"1"},
	{"id":4,"active":1},
	{"id":5,"active":false},
	{"id":6,"active":"false"},
	{"id":7,"active":"0"},
	{"id":8,"active":0},
	{"id":9,"active":""}
]

في هذه الحالة ، يشير الحقل النشط إلى نوع منطقي ووجود قيمة واحدة فقط من قيمتين: صواب وخطأ. يجب تحويل القيم غير المنطقية إلى قيمة منطقية أثناء إزالة التسلسل.

في المثال الحالي ، نعترف بالمباريات التالية. تتوافق القيم الحقيقية مع: true (منطقي) ، true (سلسلة) ، 1 (سلسلة) ، 1 (رقمية). القيمة الخاطئة تتوافق مع: false (منطقي) ، false (سلسلة) ، 0 (سلسلة) ، 0 (رقمية) ، "" (سلسلة فارغة).

أولاً ، سنعلن عن البنية المستهدفة لإزالة التسلسل. كنوع الحقل النشط ، نحدد على الفور النوع المخصص CustomBool:

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

CustomBool هي بنية بها حقل منطقي واحد من نوع bool:

type CustomBool struct {
	Bool bool
}

نقوم بتطبيق طريقة UnmarshalJSON لهذه البنية. سأعطيك الشفرة على الفور:

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

نظرًا لأن الحقل النشط في حالتنا يحتوي على عدد محدود من القيم ، يمكننا اتخاذ قرار باستخدام بنية حالة التبديل حول قيمة حقل Bool في بنية CustomBool. للتحقق ، تحتاج فقط إلى كتلتين من الحالات. في المربع الأول ، نتحقق من القيمة "true" ، في الثانية - false.

عند تسجيل القيم المحتملة ، يجب الانتباه إلى دور الحصى (هذه علامة اقتباس على المفتاح بالحرف E في تخطيط اللغة الإنجليزية). تسمح لك هذه الشخصية بالهروب من علامات الاقتباس المزدوجة في سلسلة. للتوضيح ، قمت بتأطير القيم بعلامات اقتباس وبدون علامات اقتباس مع هذا الرمز. وهكذا ، `false` يتوافق مع السلسلة false (بدون علامات الاقتباس ، اكتب bool في json) ، و` false 'يتوافق مع السلسلة "false" (مع علامات الاقتباس ، اكتب سلسلة في json). نفس الشيء مع قيم "1" و "1" `الأول هو الرقم 1 (مكتوب في json بدون علامات اقتباس) ، والثاني هو السلسلة" 1 "(في json المكتوبة بعلامات اقتباس). هذا الإدخال "" هو سلسلة فارغة ، أي في شكل json يبدو كالتالي: "".

تتم كتابة القيمة المطابقة (صواب أو خطأ) مباشرةً في الحقل Bool في بنية CustomBool:

cb.Bool = true

في الكتلة الافتراضية ، نعرض خطأ يفيد بأن الحقل له قيمة غير معروفة:

return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")

الآن يمكننا تطبيق طريقة json.Unmarshal على كود json الخاص بنا ، وسيتم تحويل قيم الحقل النشط إلى نوع بدائي بدائي.

نقوم بتطبيق طريقة MarshalJSON لبنية CustomBool:

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

لا جديد هنا. الأسلوب تسلسل حقل Bool بنية CustomBool.

إليك الشفرة الكاملة التي تعرض نتائج التسلسل وإلغاء التسلسل (تم حذف تدقيق الأخطاء للإيجاز):

package main

import (
	"encoding/json"
	"errors"
	"fmt"
)

type CustomBool struct {
	Bool bool
}

func (cb *CustomBool) UnmarshalJSON(data []byte) error {
	switch string(data) {
	case `"true"`, `true`, `"1"`, `1`:
		cb.Bool = true
		return nil
	case `"false"`, `false`, `"0"`, `0`, `""`:
		cb.Bool = false
		return nil
	default:
		return errors.New("CustomBool: parsing \"" + string(data) + "\": unknown value")
	}
}

func (cb CustomBool) MarshalJSON() ([]byte, error) {
	json, err := json.Marshal(cb.Bool)
	return json, err
}

type Target struct {
	Id     int        `json:"id"`
	Active CustomBool `json:"active"`
}

func main() {
	jsonString := `[{"id":1,"active":true},
					{"id":2,"active":"true"},
					{"id":3,"active":"1"},
					{"id":4,"active":1},
					{"id":5,"active":false},
					{"id":6,"active":"false"},
					{"id":7,"active":"0"},
					{"id":8,"active":0},
					{"id":9,"active":""}]`

	targets := []Target{}

	_ = json.Unmarshal([]byte(jsonString), &targets)

	for _, t := range targets {
		fmt.Println(t.Id, "-", t.Active.Bool)
	}

	jsonStringNew, _ := json.Marshal(targets)
	fmt.Println(string(jsonStringNew))
}

نتيجة تنفيذ التعليمات البرمجية:

1 - true
2 - true
3 - true
4 - true
5 - false
6 - false
7 - false
8 - false
9 - false
[{"id":1,"active":true},{"id":2,"active":true},{"id":3,"active":true},{"id":4,"active":true},{"id":5,"active":false},{"id":6,"active":false},{"id":7,"active":false},{"id":8,"active":false},{"id":9,"active":false}]

الموجودات


أولا. يسمح لك تجاوز طرق MarshalJSON و UnmarshalJSON لأنواع البيانات العشوائية بتخصيص التسلسل وإلغاء تسلسل حقل كود json محدد. بالإضافة إلى حالات الاستخدام المشار إليها ، يتم استخدام هذه الوظائف للعمل مع الحقول الخالية.

ثانيا. تنسيق ترميز json text هو أداة مستخدمة على نطاق واسع لتبادل المعلومات ، وإحدى مزاياها على التنسيقات الأخرى هي توافر أنواع البيانات. يجب مراقبة الامتثال لهذه الأنواع بدقة.

All Articles