Los: JSON-Deserialisierung mit falscher Eingabe oder Umgehen von API-Entwicklerfehlern

Bild

Kürzlich habe ich auf Go einen http-Client für einen Dienst entwickelt, der eine REST-API mit json als Codierungsformat bereitstellt. Eine Standardaufgabe, aber im Laufe der Arbeit musste ich mich einem nicht standardmäßigen Problem stellen. Ich sage dir, worum es geht.

Wie Sie wissen, hat das JSON-Format Datentypen. Vier Grundelemente: Zeichenfolge, Zahl, Boolescher Wert, Null; und zwei Strukturtypen: ein Objekt und ein Array. In diesem Fall interessieren uns primitive Typen. Hier ist ein Beispiel für einen JSON-Code mit vier Feldern unterschiedlichen Typs:

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

Wie das Beispiel zeigt, wird der Zeichenfolgenwert in Anführungszeichen gesetzt. Numerisch - hat keine Anführungszeichen. Ein boolescher Typ kann nur einen von zwei Werten haben: true oder false (ohne Anführungszeichen). Und der Null-Typ ist dementsprechend null (auch ohne Anführungszeichen).

Und jetzt das Problem selbst. Irgendwann stellte ich bei einer detaillierten Untersuchung des von einem Drittanbieter-Service erhaltenen JSON-Codes fest, dass eines der Felder (nennen wir es Preis) regelmäßig einen Zeichenfolgenwert (die Zahl in Anführungszeichen) zusätzlich zum numerischen Wert enthält. Das heißt, dieselbe Abfrage mit unterschiedlichen Parametern kann eine Zahl als Zahl oder dieselbe Zahl als Zeichenfolge zurückgeben. Ich kann mir nicht vorstellen, wie der Code, der solche Ergebnisse zurückgibt, am anderen Ende organisiert ist, aber anscheinend liegt dies daran, dass der Dienst selbst ein Aggregator ist und Daten aus verschiedenen Quellen abruft und die Entwickler die Serverantwort json nicht auf ein einziges Format gebracht haben. Trotzdem ist es notwendig, mit dem zu arbeiten, was ist.

Aber dann war ich noch mehr überrascht. Das logische Feld (nennen wir es aktiv) gab zusätzlich zu true und false die Zeichenfolgenwerte "true", "false" und sogar die numerischen Werte 1 und 0 (true bzw. false) zurück.

All diese Verwirrung über Datentypen wäre nicht kritisch, wenn ich json say in schwach typisiertem PHP verarbeiten würde, aber Go hat eine starke Typisierung und erfordert eine klare Angabe des Typs des desialisierten Feldes. Infolgedessen musste ein Mechanismus implementiert werden, mit dem alle Werte des aktiven Felds während des Deserialisierungsprozesses in einen logischen Typ und jeder Wert des Preisfelds in einen numerischen konvertiert werden können.

Beginnen wir mit dem Preisfeld.

Angenommen, wir haben folgenden JSON-Code:

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

Das heißt, json enthält ein Array von Objekten mit zwei Feldern eines numerischen Typs. Der Standard-Deserialisierungscode für diesen json on Go sieht folgendermaßen aus:

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

In diesem Code deserialisieren wir das ID-Feld in int und das Preisfeld in float64. Nehmen wir nun an, unser JSON-Code sieht folgendermaßen aus:

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

Das heißt, das Preisfeld enthält Werte sowohl eines numerischen Typs als auch einer Zeichenfolge. In diesem Fall können nur die numerischen Werte des Preisfelds in den Typ float64 dekodiert werden, während Zeichenfolgenwerte einen Fehler bezüglich der Inkompatibilität von Typen verursachen. Dies bedeutet, dass weder float64 noch ein anderer primitiver Typ zum Deserialisieren dieses Felds geeignet ist, und wir benötigen unseren eigenen benutzerdefinierten Typ mit eigener Deserialisierungslogik.

Deklarieren Sie als solchen Typ eine CustomFloat64-Struktur mit einem einzelnen Float64-Feld vom Typ float64.

type CustomFloat64 struct{
	Float64 float64
}

Geben Sie diesen Typ sofort für das Feld Preis in der Zielstruktur an:

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

Jetzt müssen Sie Ihre eigene Logik zum Dekodieren eines Felds vom Typ CustomFloat64 beschreiben.

Das Paket "encoding / json" verfügt über zwei spezielle Methoden: MarshalJSON und UnmarshalJSON , mit denen die Codierungs- und Decodierungslogik eines bestimmten Benutzerdatentyps angepasst werden kann. Es reicht aus, diese Methoden zu überschreiben und Ihre eigene Implementierung zu beschreiben.

Überschreiben Sie die UnmarshalJSON-Methode für einen beliebigen Typ CustomFloat64. In diesem Fall muss die Signatur der Methode genau befolgt werden, da sie sonst einfach nicht funktioniert und vor allem keinen Fehler verursacht.

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

Bei der Eingabe nimmt diese Methode eine Schicht von Bytes (Daten), die den Wert eines bestimmten Feldes des decodierten JSON enthält. Wenn wir diese Folge von Bytes in eine Zeichenfolge konvertieren, sehen wir den Wert des Feldes genau in der Form, in der es in json geschrieben ist. Das heißt, wenn es sich um einen Zeichenfolgentyp handelt, wird genau eine Zeichenfolge mit doppelten Anführungszeichen („258“) angezeigt. Wenn es sich um einen numerischen Typ handelt, wird eine Zeichenfolge ohne Anführungszeichen (258) angezeigt.

Um einen numerischen Wert von einem Zeichenfolgenwert zu unterscheiden, müssen Sie prüfen, ob das erste Zeichen ein Anführungszeichen ist. Da das doppelte Anführungszeichen in der UNICODE-Tabelle ein Byte belegt, müssen wir nur das erste Byte des Daten-Slice überprüfen, indem wir es mit der Zeichennummer in UNICODE vergleichen. Dies ist Nummer 34. Beachten Sie, dass ein Zeichen im Allgemeinen nicht einem Byte entspricht, da es mehr als ein Byte benötigen kann. Ein Charakter in Go entspricht Rune (Rune). In unserem Fall ist diese Bedingung ausreichend:

if data[0] == 34 {

Wenn die Bedingung erfüllt ist, hat der Wert einen Zeichenfolgentyp, und wir müssen die Zeichenfolge zwischen den Anführungszeichen abrufen, dh das Slice-Byte zwischen dem ersten und dem letzten Byte. Dieses Slice enthält einen numerischen Wert, der in den primitiven Typ float64 dekodiert werden kann. Dies bedeutet, dass wir die Methode json.Unmarshal darauf anwenden können, während das Ergebnis in der CustomFloat64-Struktur im Feld Float64 gespeichert wird.

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

Wenn das Daten-Slice nicht mit einem Anführungszeichen beginnt, enthält es bereits einen numerischen Datentyp, und wir können die Methode json.Unmarshal direkt auf das gesamte Daten-Slice anwenden.

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

Hier ist der vollständige Code für die UnmarshalJSON-Methode:

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
}

Infolgedessen werden unter Verwendung der json.Unmarshal-Methode für unseren json-Code alle Werte des Preisfelds für uns transparent in einen primitiven Typ float64 konvertiert und das Ergebnis in das Float64-Feld der CustomFloat64-Struktur geschrieben.

Jetzt müssen wir möglicherweise die Zielstruktur wieder in json konvertieren. Wenn wir jedoch die json.Marshal-Methode direkt auf den CustomFloat64-Typ anwenden, serialisieren wir diese Struktur als Objekt. Wir müssen das Preisfeld in einen numerischen Wert kodieren. Um die Codierungslogik des benutzerdefinierten Typs CustomFloat64 anzupassen, implementieren wir die MarshalJSON-Methode dafür, wobei wir die Signatur der Methode genau beachten:

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

Bei dieser Methode müssen Sie lediglich die json.Marshal-Methode erneut verwenden, sie jedoch bereits nicht auf die CustomFloat64-Struktur, sondern auf das Float64-Feld anwenden. Von der Methode geben wir das empfangene Byte Slice und den Fehler zurück.

Hier ist der vollständige Code, der die Ergebnisse der Serialisierung und Deserialisierung anzeigt (die Fehlerprüfung wird der Kürze halber weggelassen, die Nummer des Bytes mit dem doppelten Anführungszeichen ist konstant):

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

Ergebnis der Codeausführung:

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

Fahren wir mit dem zweiten Teil fort und implementieren denselben Code für die JSON-Deserialisierung mit inkonsistenten Werten des logischen Felds.

Angenommen, wir haben folgenden JSON-Code:

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

In diesem Fall impliziert das aktive Feld einen logischen Typ und das Vorhandensein von nur einem von zwei Werten: wahr und falsch. Nicht-boolesche Werte müssen während der Deserialisierung in boolesche Werte konvertiert werden.

Im aktuellen Beispiel geben wir die folgenden Übereinstimmungen zu. Wahre Werte entsprechen: wahr (logisch), wahr (Zeichenfolge), 1 (Zeichenfolge), 1 (numerisch). Der falsche Wert entspricht: false (logisch), false (Zeichenfolge), 0 (Zeichenfolge), 0 (numerisch), "" (leere Zeichenfolge).

Zunächst deklarieren wir die Zielstruktur für die Deserialisierung. Als Typ des Felds Aktiv geben wir sofort den benutzerdefinierten Typ CustomBool an:

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

CustomBool ist eine Struktur mit einem einzelnen Bool-Feld vom Typ Bool:

type CustomBool struct {
	Bool bool
}

Wir implementieren die UnmarshalJSON-Methode für diese Struktur. Ich gebe Ihnen sofort den Code:

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

Da das aktive Feld in unserem Fall eine begrenzte Anzahl von Werten hat, können wir das switch-case-Konstrukt verwenden, um zu entscheiden, wie der Wert des Bool-Felds der CustomBool-Struktur sein soll. Zur Überprüfung benötigen Sie nur zwei Fallblöcke. Im ersten Block überprüfen wir den Wert auf true, im zweiten auf false.

Wenn Sie mögliche Werte schreiben, sollten Sie auf die Rolle des Kieses achten (dies ist ein solches Anführungszeichen auf dem Schlüssel mit dem Buchstaben E im englischen Layout). Mit diesem Zeichen können Sie doppelte Anführungszeichen in einer Zeichenfolge umgehen. Aus Gründen der Übersichtlichkeit habe ich die Werte mit Anführungszeichen und ohne Anführungszeichen mit diesem Symbol versehen. Somit entspricht "false" der Zeichenfolge false (ohne Anführungszeichen, geben Sie bool in json ein), und "false" entspricht der Zeichenfolge "false" (mit Anführungszeichen, geben Sie string in json ein). Das Gleiche gilt für die Werte "1" und "1". Die erste ist die Zahl 1 (in json ohne Anführungszeichen geschrieben), die zweite ist die Zeichenfolge "1" (in json mit Anführungszeichen). Dieser Eintrag `` "` ist eine leere Zeichenfolge, d. H. Im JSON-Format sieht er folgendermaßen aus: "".

Der entsprechende Wert (true oder false) wird direkt in das Bool-Feld der CustomBool-Struktur geschrieben:

cb.Bool = true

Im Standardblock geben wir einen Fehler zurück, der besagt, dass das Feld einen unbekannten Wert hat:

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

Jetzt können wir die json.Unmarshal-Methode auf unseren json-Code anwenden, und die Werte des aktiven Felds werden in einen primitiven Bool-Typ konvertiert.

Wir implementieren die MarshalJSON-Methode für die CustomBool-Struktur:

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

Hier gibt es nichts Neues. Die Methode serialisiert das Bool-Feld der CustomBool-Struktur.

Hier ist der vollständige Code, der die Ergebnisse der Serialisierung und Deserialisierung anzeigt (Fehlerprüfung der Kürze halber weggelassen):

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

Ergebnis der Codeausführung:

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

Ergebnisse


Erstens. Durch Überschreiben der Methoden MarshalJSON und UnmarshalJSON für beliebige Datentypen können Sie die Serialisierung und Deserialisierung eines bestimmten JSON-Codefelds anpassen. Zusätzlich zu den angegebenen Anwendungsfällen werden diese Funktionen verwendet, um mit nullbaren Feldern zu arbeiten.

Zweitens. Das json-Textcodierungsformat ist ein weit verbreitetes Werkzeug für den Informationsaustausch. Einer seiner Vorteile gegenüber anderen Formaten ist die Verfügbarkeit von Datentypen. Die Einhaltung dieser Typen muss streng überwacht werden.

All Articles