Let's Go Further پیوست‌ها › نکات دقیق کدگذاری JSON
قبلی · فهرست مطالب · بعدی
فصل ۲۱.۴.

نکات دقیق کدگذاری JSON

Slie‌های nil و خالی به صورت متفاوت کدگذاری می‌شوند

Slie‌های nil در Go به مقدار JSON null کدگذاری می‌شوند، در حالی که یک slie خالی (اما نه nil) به آرایه JSON خالی کدگذاری می‌شود. به عنوان مثال:

var nilSlice []string
emptySlice := []string{}

m := map[string][]string{
    "nilSlice":   nilSlice,
    "emptySlice": emptySlice,
}

به JSON زیر کدگذاری می‌شود:

{"emptySlice":[],"nilSlice":null}

کاراکترهای نگارشی غیر ASCII در struct tag‌ها پشتیبانی نمی‌شوند

هنگام استفاده از struct tag‌ها برای تغییر کلیدها در یک شیء JSON، هر تگی که حاوی کاراکترهای نگارشی غیر ASCII باشد نادیده گرفته می‌شود. به طور قابل توجهی این بدان معناست که نمی‌توانید از خط فاصله en یا em، یا بیشتر نشانه‌های ارزی در struct tag‌ها استفاده کنید. به عنوان مثال، struct زیر:

s := struct {
    CostUSD string `json:"cost $"` // This is OK.
    CostEUR string `json:"cost €"` // This contains the non-ASCII punctuation character 
                                   // € and will be ignored.
}{
    CostUSD: "100.00",
    CostEUR: "100.00",
}

به JSON زیر کدگذاری می‌شود (توجه کنید که struct tag تغییر نام کلید CostEUR نادیده گرفته شده است):

{"cost $":"100.00","CostEUR":"100.00"}

مقادیر integer، time.Time و net.IP می‌توانند به عنوان کلیدهای map استفاده شوند

امکان کدگذاری یک map که مقادیر integer به عنوان کلیدهای map دارد وجود دارد. اعداد صحیح به طور خودکار در JSON حاصل به رشته‌ها تبدیل می‌شوند (زیرا کلیدها در یک شیء JSON همیشه باید رشته باشند). به عنوان مثال، map زیر:

m := map[int]string{
    123: "foo",
    456_000: "bar",
}

به JSON زیر کدگذاری می‌شود:

{"123":"foo","456000":"bar"}

علاوه بر این، کلیدهای map که interface encoding.TextMarshaler را پیاده‌سازی می‌کنند نیز پشتیبانی می‌شوند. این بدان معناست که می‌توانید مقادیر time.Time و net.IP را نیز به طور پیش‌فرض به عنوان کلیدهای map استفاده کنید. به عنوان مثال، map زیر:

t1 := time.Now()
t2 := t1.Add(24 * time.Hour)

m := map[time.Time]string{
    t1: "foo",
    t2: "bar",
}

به JSON کدگذاری می‌شود که مشابه این است:

{"2009-11-10T23:00:00Z":"foo","2009-11-11T23:00:00Z":"bar"}

زوایا و & در رشته‌ها اسکیپ می‌شوند

اگر یک رشته حاوی زوایا < یا > باشد، هنگام کدگذاری به JSON به کدهای کاراکتر یونیکد \u003c و \u003e اسکیپ می‌شوند. به همین ترتیب کاراکتر & به \u0026 اسکیپ می‌شود. این برای جلوگیری از تفسیر تصادفی پاسخ JSON به عنوان HTML توسط برخی مرورگرها است. به عنوان مثال، slie زیر:

s := []string{
    "<foo>",
    "bar & baz",
}

به JSON زیر کدگذاری می‌شود:

["\u003cfoo\u003e","bar \u0026 baz"]

اگر می‌خواهید از اسکیپ شدن این کاراکترها جلوگیری کنید، باید از یک نمونه json.Encoder با SetEscapeHTML(false) برای انجام کدگذاری استفاده کنید.

صفرهای انتهایی از اعداد اعشاری حذف می‌شوند

هنگام کدگذاری یک عدد اعشاری با بخش اعشاری که به صفر ختم می‌شود، هر صفر انتهایی در JSON ظاهر نمی‌شود. به عنوان مثال:

s := []float64{
    123.0,
    456.100,
    789.990,
}

به JSON زیر کدگذاری می‌شود:

[123,456.1,789.99]

کار با JSON از پیش محاسبه شده

اگر یک رشته یا slie از نوع []byte دارید که حاوی JSON «از پیش محاسبه شده» یا «از پیش کدگذاری شده» است، به طور پیش‌فرض Go آن را درست مانند هر رشته یا slie دیگری از نوع []byte در هنگام کدگذاری رفتار می‌کند. این بدان معناست که یک رشته اسکیپ و به عنوان یک رشته JSON کدگذاری می‌شود، و یک slie بایت به عنوان یک رشته JSON base64 کدگذاری می‌شود. به عنوان مثال، struct زیر:

m := struct {
    Person string
}{
    Person: `{"name": "Alice", "age": 21}`,
}

به این صورت کدگذاری می‌شود:

{"Person":"{\"name\": \"Alice\", \"age\": 21}"}

اگر می‌خواهید JSON از پیش محاسبه شده را بدون هیچ تغییری جایگزین کنید، باید مقدار JSON از پیش محاسبه شده را به نوع json.RawMessage تبدیل کنید. سپس Go آن را مستقیماً در بقیه JSON جایگزین می‌کند. به عنوان مثال:

m := struct {
    Person json.RawMessage
}{
    Person: json.RawMessage(`{"name": "Alice", "age": 21}`),
}

به JSON زیر کدگذاری می‌شود:

{"Person":{"name":"Alice","age":21}}

جایگزین MarshalText

اگر یک نوع متد MarshalJSON() ندارد اما در عوض متد MarshalText() دارد (به طوری که interface encoding.TextMarshaler را پیاده‌سازی کند)، Go در هنگام کدگذاری JSON به این روش بازمی‌گردد و نتیجه را به عنوان یک رشته JSON ارائه می‌دهد.

به عنوان مثال، اگر کد زیر را اجرا کنید:

type myFloat float64

func (f myFloat) MarshalText() ([]byte, error) {
    return []byte(fmt.Sprintf("%.2f", f)), nil    
}

func main() {
    f := myFloat(1.0/3.0)
    
    js, err := json.Marshal(f)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", js)
}

مقدار بازگشتی از MarshalText() را به عنوان یک رشته JSON چاپ می‌کند:

"0.33"

گیرنده هنگام استفاده از MarshalJSON اهمیت دارد

قبلاً در کتاب به این موضوع اشاره کردیم، اما واقعاً ارزش تأکید دارد زیرا اغلب افراد را گمراه می‌کند. اگر متد MarshalJSON() را روی یک نوع سفارشی پیاده‌سازی کنید و متد MarshalJSON() از یک گیرنده اشاره‌گر استفاده کند، فقط هنگام کدگذاری یک اشاره‌گر به نوع سفارشی استفاده می‌شود. برای هر مقدار نوع سفارشی، کاملاً نادیده گرفته می‌شود.

می‌توانید این را در عمل ببینید اگر این کد را در Go playground اجرا کنید.

مگر اینکه به طور خاص این رفتار را بخواهید، توصیه می‌کنم عادت کنید که همیشه از گیرنده‌های مقداری برای متد MarshalJSON() استفاده کنید، درست مانند آنچه در این کتاب داشته‌ایم.