Go: deserialisasi JSON dengan pengetikan yang salah, atau cara mengatasi kesalahan pengembang API

gambar

Baru-baru ini, saya kebetulan mengembangkan klien http on Go untuk layanan yang menyediakan REST API dengan json sebagai format penyandian. Tugas standar, tetapi dalam pekerjaan saya harus menghadapi masalah non-standar. Saya memberi tahu Anda apa intinya.

Seperti yang Anda ketahui, format json memiliki tipe data. Empat primitif: string, angka, boolean, null; dan dua tipe struktural: objek dan array. Dalam hal ini, kami tertarik pada tipe primitif. Berikut adalah contoh kode json dengan empat bidang tipe yang berbeda:

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

Seperti yang ditunjukkan contoh, nilai string dilampirkan dalam tanda kutip. Numerik - tidak memiliki tanda kutip. Tipe boolean hanya dapat memiliki satu dari dua nilai: true atau false (tanpa tanda kutip). Dan tipe null sesuai nol (juga tanpa tanda kutip).

Dan sekarang masalahnya sendiri. Pada titik tertentu, dalam pemeriksaan terperinci atas kode json yang diterima dari layanan pihak ketiga, saya menemukan bahwa salah satu bidang (sebut saja harga) secara berkala memiliki nilai string (jumlah dalam tanda kutip) di samping nilai numerik. Artinya, kueri yang sama dengan parameter yang berbeda dapat mengembalikan angka sebagai angka, atau dapat mengembalikan angka yang sama sebagai string. Saya tidak bisa membayangkan bagaimana kode yang mengembalikan hasil tersebut diatur di ujung yang lain, tetapi tampaknya ini disebabkan oleh fakta bahwa layanan itu sendiri adalah agregator dan menarik data dari sumber yang berbeda, dan pengembang tidak membawa respons server json ke format tunggal. Meskipun demikian, perlu untuk bekerja dengan apa yang ada.

Tetapi kemudian saya bahkan lebih terkejut. Bidang logis (sebut saja aktif), selain benar dan salah, mengembalikan nilai string "benar", "salah", dan bahkan numerik 1 dan 0 (masing-masing benar dan salah).

Semua kebingungan tentang tipe data ini tidak akan menjadi kritis jika saya akan memproses json mengatakan dalam PHP yang diketik dengan lemah, tetapi Go memiliki pengetikan yang kuat, dan membutuhkan indikasi yang jelas tentang jenis bidang deserialized. Akibatnya, ada kebutuhan untuk menerapkan mekanisme yang memungkinkan mengkonversi semua nilai dari bidang aktif ke tipe logis selama proses deserialisasi, dan nilai apa pun dari bidang harga ke yang numerik.

Mari kita mulai dengan kolom harga.

Misalkan kita memiliki kode json seperti ini:

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

Yaitu, json berisi larik objek dengan dua bidang bertipe numerik. Kode deserialisasi standar untuk json on Go ini terlihat seperti ini:

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)
	}
}

Dalam kode ini, kita akan membatalkan desalisasi bidang id ke int dan bidang harga ke float64. Sekarang anggaplah kode json kita terlihat seperti ini:

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

Yaitu, bidang harga berisi nilai tipe numerik dan string. Dalam kasus ini, hanya nilai numerik bidang harga yang dapat diterjemahkan ke dalam tipe float64, sementara nilai string akan menyebabkan kesalahan tentang ketidakcocokan jenis. Ini berarti bahwa float64 atau tipe primitif lainnya tidak cocok untuk deserialisasi bidang ini, dan kita membutuhkan tipe kustom kita sendiri dengan logika deserialisasi sendiri.

Sebagai tipe seperti itu, deklarasikan struktur CustomFloat64 dengan bidang Float64 tunggal dari tipe float64.

type CustomFloat64 struct{
	Float64 float64
}

Dan segera tunjukkan jenis ini untuk bidang Harga dalam struktur Target:

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

Sekarang Anda perlu menjelaskan logika Anda sendiri untuk mendekode bidang tipe CustomFloat64.

Paket encoding / json menyediakan dua metode khusus: MarshalJSON dan UnmarshalJSON , yang dirancang untuk menyesuaikan logika enkode dan dekode dari tipe data pengguna tertentu. Cukup dengan mengganti metode ini dan menjelaskan implementasi Anda sendiri.

Ganti metode UnmarshalJSON untuk jenis CustomFloat64 yang sewenang-wenang. Dalam hal ini, sangat penting untuk mengikuti tanda tangan metode ini, jika tidak maka metode ini tidak akan berfungsi, dan yang paling penting itu tidak akan menghasilkan kesalahan.

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

Pada input, metode ini mengambil sepotong byte (data), yang berisi nilai bidang tertentu dari json yang didekodekan. Jika kita mengonversi urutan byte ini menjadi string, maka kita akan melihat nilai bidang persis dalam bentuk yang ditulis dalam json. Yaitu, jika itu adalah tipe string, maka kita akan melihat string dengan tanda kutip ganda ("258"), jika itu adalah tipe numerik, maka kita akan melihat string tanpa tanda kutip (258).

Untuk membedakan nilai numerik dari nilai string, Anda harus memeriksa apakah karakter pertama adalah tanda kutip. Karena karakter kutipan ganda dalam tabel UNICODE membutuhkan satu byte, kita hanya perlu memeriksa byte pertama dari irisan data dengan membandingkannya dengan nomor karakter di UNICODE. Ini adalah angka 34. Perhatikan bahwa secara umum, karakter tidak sama dengan byte, karena dapat mengambil lebih dari satu byte. Simbol dalam Go sama dengan rune (rune). Dalam kasus kami, kondisi ini cukup:

if data[0] == 34 {

Jika kondisi terpenuhi, maka nilainya memiliki tipe string, dan kita perlu mendapatkan string di antara tanda kutip, yaitu byte slice antara byte pertama dan terakhir. Iris ini berisi nilai numerik yang dapat diterjemahkan ke dalam tipe float64 primitif. Ini berarti bahwa kita dapat menerapkan metode json.Unmarshal padanya, sambil menyimpan hasilnya di bidang Float64 dari struktur CustomFloat64.

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

Jika irisan data tidak dimulai dengan tanda kutip, maka sudah berisi tipe data numerik, dan kita bisa menerapkan metode json.Unmarshal langsung ke seluruh irisan data.

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

Berikut adalah kode lengkap untuk metode 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
}

Akibatnya, menggunakan metode json.Unmarshal ke kode json kami, semua nilai bidang harga akan secara transparan dikonversi ke tipe float64 primitif untuk kami, dan hasilnya akan ditulis ke bidang Float64 pada struktur CustomFloat64.

Sekarang kita mungkin perlu mengubah struktur Target kembali ke json. Tapi, jika kita menerapkan metode json.Marshal langsung ke tipe CustomFloat64, maka kita membuat serial struktur ini sebagai objek. Kita perlu menyandikan bidang harga menjadi nilai numerik. Untuk mengkustomisasi logika pengkodean dari tipe kustom CustomFloat64, kami menerapkan metode MarshalJSON untuk itu, sambil secara ketat mengamati tanda tangan metode:

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

Yang perlu Anda lakukan dalam metode ini adalah kembali menggunakan metode json.Marshal, tetapi sudah menerapkannya bukan pada struktur CustomFloat64, tetapi ke bidang Float64. Dari metode ini, kita mengembalikan byte dan slice yang diterima.

Berikut ini adalah kode lengkap yang menampilkan hasil serialisasi dan deserialisasi (pengecekan kesalahan dihilangkan untuk singkatnya, jumlah byte dengan simbol tanda kutip ganda konstan):

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))
}

Hasil Eksekusi Kode:

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}]

Mari kita beralih ke bagian kedua dan mengimplementasikan kode yang sama untuk deserialisasi json dengan nilai yang tidak konsisten dari bidang logis.

Misalkan kita memiliki kode json seperti ini:

[
	{"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":""}
]

Dalam kasus ini, bidang aktif menyiratkan tipe logis dan keberadaan hanya satu dari dua nilai: true dan false. Nilai-nilai non-boolean perlu dikonversi ke boolean selama deserialisasi.

Dalam contoh saat ini, kami mengakui kecocokan berikut. Nilai true berhubungan dengan: true (logis), true (string), 1 (string), 1 (numeric). Nilai false sesuai dengan: false (logical), false (string), 0 (string), 0 (numeric), "" (string kosong).

Pertama, kami akan mendeklarasikan struktur target untuk deserialisasi. Sebagai jenis bidang Aktif, kami segera menentukan jenis khusus CustomBool:

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

CustomBool adalah struktur dengan satu bidang bool tunggal dari tipe bool:

type CustomBool struct {
	Bool bool
}

Kami menerapkan metode UnmarshalJSON untuk struktur ini. Saya akan segera memberi Anda kode:

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")
	}
}

Karena bidang aktif dalam kasing kami memiliki jumlah nilai yang terbatas, kami dapat membuat keputusan dengan menggunakan sakelar sakelar bertukar tentang berapa nilai bidang Bool dari struktur CustomBool. Untuk memeriksa, Anda hanya perlu dua blok kasus. Di blok pertama, kami memeriksa nilai true, di blok kedua - false.

Saat merekam nilai yang mungkin, Anda harus memperhatikan peran kerikil (ini adalah tanda kutip pada tombol dengan huruf E dalam tata letak bahasa Inggris). Karakter ini memungkinkan Anda menghindari tanda kutip ganda dalam sebuah string. Untuk lebih jelasnya, saya membingkai nilai dengan tanda kutip dan tanpa tanda kutip dengan simbol ini. Jadi, `false` sesuai dengan string false (tanpa tanda kutip, ketik bool in json), dan` false 'sesuai dengan string "false" (dengan tanda kutip, ketik string dalam json). Hal yang sama dengan nilai `1` dan` 1` `Yang pertama adalah angka 1 (ditulis dalam json tanpa tanda kutip), yang kedua adalah string" 1 "(dalam json ditulis dengan tanda kutip). Entri ini `` "` adalah string kosong, mis., Dalam format json terlihat seperti ini: "".

Nilai yang sesuai (benar atau salah) ditulis langsung ke bidang Bool dari struktur CustomBool:

cb.Bool = true

Di blok defaul, kami mengembalikan kesalahan yang menyatakan bahwa bidang memiliki nilai yang tidak diketahui:

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

Sekarang kita dapat menerapkan metode json.Unmarshal ke kode json kita, dan nilai-nilai bidang aktif akan dikonversi ke tipe bool primitif.

Kami menerapkan metode MarshalJSON untuk struktur CustomBool:

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

Tidak ada yang baru di sini. Metode ini membuat serial bidang Bool dari struktur CustomBool.

Berikut adalah kode lengkap yang menampilkan hasil serialisasi dan deserialisasi (pengecekan kesalahan dihilangkan untuk singkatnya):

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))
}

Hasil Eksekusi Kode:

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}]

temuan


Pertama. Mengganti metode MarshalJSON dan UnmarshalJSON untuk tipe data arbitrer memungkinkan Anda untuk menyesuaikan serialisasi dan deserialisasi bidang kode json tertentu. Selain kasus penggunaan yang ditunjukkan, fungsi-fungsi ini digunakan untuk bekerja dengan bidang nullable.

Kedua. Format penyandian teks json adalah alat yang banyak digunakan untuk bertukar informasi, dan salah satu kelebihannya dari format lain adalah ketersediaan tipe data. Kepatuhan dengan tipe-tipe ini harus dipantau dengan ketat.

All Articles