Gestion de la demande de publication JSON dans Go

250

J'ai donc ce qui suit, qui semble incroyablement hacky, et je me suis dit que Go a des bibliothèques mieux conçues que cela, mais je ne trouve pas d'exemple de Go traitant une demande POST de données JSON. Ce sont tous des POST.

Voici un exemple de demande: curl -X POST -d "{\"test\": \"that\"}" http://localhost:8082/test

Et voici le code, avec les journaux intégrés:

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type test_struct struct {
    Test string
}

func test(rw http.ResponseWriter, req *http.Request) {
    req.ParseForm()
    log.Println(req.Form)
    //LOG: map[{"test": "that"}:[]]
    var t test_struct
    for key, _ := range req.Form {
        log.Println(key)
        //LOG: {"test": "that"}
        err := json.Unmarshal([]byte(key), &t)
        if err != nil {
            log.Println(err.Error())
        }
    }
    log.Println(t.Test)
    //LOG: that
}

func main() {
    http.HandleFunc("/test", test)
    log.Fatal(http.ListenAndServe(":8082", nil))
}

Il doit y avoir un meilleur moyen, non? Je suis juste perplexe pour trouver quelle pourrait être la meilleure pratique.

(Go est également connu sous le nom de Golang pour les moteurs de recherche, et mentionné ici pour que d'autres puissent le trouver.)

TomJ
la source
3
si vous utilisez curl -X POST -H 'Content-Type: application/json' -d "{\"test\": \"that\"}", alors req.Form["test"]devrait revenir"that"
Vinicius
@Vinicius y a-t-il des preuves de cela?
diralik

Réponses:

388

Veuillez utiliser à la json.Decoderplace de json.Unmarshal.

func test(rw http.ResponseWriter, req *http.Request) {
    decoder := json.NewDecoder(req.Body)
    var t test_struct
    err := decoder.Decode(&t)
    if err != nil {
        panic(err)
    }
    log.Println(t.Test)
}
Joe
la source
79
Pourriez-vous expliquer pourquoi?
Ryan Bigg
86
Pour commencer, il semble que cela puisse gérer un flux plutôt que d'avoir besoin de tout charger vous-même dans un tampon. (Je suis un autre Joe BTW)
Joe
7
Je me demande à quoi ressemblerait une gestion des erreurs appropriée dans ce cas. Je ne pense pas que ce soit une bonne idée de paniquer sur un json invalide.
codepushr
15
Je ne pense pas que vous ayez besoin de defer req.Body.Close()De la documentation: "Le serveur fermera le corps de la demande. Le gestionnaire ServeHTTP n'a pas besoin de le faire." Aussi pour répondre à @thisisnotabus, dans la documentation: "Pour les demandes de serveur, le corps de la demande est toujours non nul mais retournera EOF immédiatement quand aucun corps n'est présent" golang.org/pkg/net/http/#Request
Drew LeSueur
22
Je suggère de ne pas utiliser json.Decoder. Il est destiné aux flux d'objets JSON, pas à un seul objet. Il n'est pas plus efficace pour un seul objet JSON car il lit l'intégralité de l'objet en mémoire. Il a un inconvénient: si des déchets sont inclus après l'objet, ils ne se plaindront pas. En fonction de quelques facteurs, il se json.Decoderpeut que le corps ne soit pas entièrement lu et que la connexion ne puisse pas être réutilisée.
Kale B
85

Vous devez lire req.Body. La ParseFormméthode est en train de lire le req.Bodypuis de l'analyser au format codé HTTP standard. Ce que vous voulez, c'est lire le corps et l'analyser au format JSON.

Voici votre code mis à jour.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "io/ioutil"
)

type test_struct struct {
    Test string
}

func test(rw http.ResponseWriter, req *http.Request) {
    body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        panic(err)
    }
    log.Println(string(body))
    var t test_struct
    err = json.Unmarshal(body, &t)
    if err != nil {
        panic(err)
    }
    log.Println(t.Test)
}

func main() {
    http.HandleFunc("/test", test)
    log.Fatal(http.ListenAndServe(":8082", nil))
}
Daniel
la source
Merci! Je vois où je me trompais maintenant. Si vous appelez req.ParseForm(), ce que je faisais dans des tentatives précédentes pour essayer de résoudre ce problème, avant d'essayer de lire le req.Body, il semble effacer le corps et unexpected end of JSON inputest jeté lorsque vous allez à Unmarshal(au moins en 1.0.2)
TomJ
1
@Daniel: Quand je fais curl -X POST -d "{\" tes \ ": \" que \ "}" localhost: 8082 / test , log.Println (t.Test) renvoie vide. Pourquoi ? Ou d'ailleurs si vous postez un autre JSON, il revient vide
Somesh
Votre demande POST est incorrecte. tes! = test. Appréciez il y a 5 ans: /
Rambatino
Ceci est un bel exemple simple!
15412
C'est un bon conseil, mais pour être clair, les réponses concernant l'utilisation de json.NewDecoder(req.Body)sont également correctes.
riche
59

Je me rendais fou avec ce problème exact. Mon Marshaller JSON et Unmarshaller ne remplissaient pas ma structure Go. J'ai ensuite trouvé la solution sur https://eager.io/blog/go-and-json :

"Comme pour toutes les structures de Go, il est important de se rappeler que seuls les champs avec une première lettre majuscule sont visibles pour les programmes externes comme le Marshaller JSON."

Après cela, mon Marshaller et Unmarshaller ont parfaitement fonctionné!

Steve Stilson
la source
Veuillez inclure quelques extraits du lien. S'il est obsolète, les exemples seront perdus.
030
47

Il y a deux raisons pour lesquelles json.Decoderil faut privilégier json.Unmarshal- qui ne sont pas abordées dans la réponse la plus populaire de 2013:

  1. Février 2018, a go 1.10introduit une nouvelle méthode json.Decoder.DisallowUnknownFields () qui répond au souci de détecter les entrées JSON indésirables
  2. req.Body est déjà un io.Reader . La lecture de l'intégralité de son contenu, puis l'exécution json.Unmarshalgaspille des ressources si le flux était, disons un bloc de 10 Mo de JSON invalide. L'analyse du corps de la demande, avec json.Decoder, lors de son flux , déclencherait une erreur d'analyse précoce si un JSON non valide était rencontré. Le traitement des flux d'E / S en temps réel est la solution préférée .

Répondre à certains des commentaires des utilisateurs sur la détection de mauvaises entrées utilisateur:

Pour appliquer les champs obligatoires et les autres vérifications d'assainissement, essayez:

d := json.NewDecoder(req.Body)
d.DisallowUnknownFields() // catch unwanted fields

// anonymous struct type: handy for one-time use
t := struct {
    Test *string `json:"test"` // pointer so we can test for field absence
}{}

err := d.Decode(&t)
if err != nil {
    // bad JSON or unrecognized json field
    http.Error(rw, err.Error(), http.StatusBadRequest)
    return
}

if t.Test == nil {
    http.Error(rw, "missing field 'test' from JSON object", http.StatusBadRequest)
    return
}

// optional extra check
if d.More() {
    http.Error(rw, "extraneous data after JSON object", http.StatusBadRequest)
    return
}

// got the input we expected: no more, no less
log.Println(*t.Test)

Terrain de jeux

Sortie typique:

$ curl -X POST -d "{}" http://localhost:8082/strict_test

expected json field 'test'

$ curl -X POST -d "{\"Test\":\"maybe?\",\"Unwanted\":\"1\"}" http://localhost:8082/strict_test

json: unknown field "Unwanted"

$ curl -X POST -d "{\"Test\":\"oops\"}g4rB4g3@#$%^&*" http://localhost:8082/strict_test

extraneous data after JSON

$ curl -X POST -d "{\"Test\":\"Works\"}" http://localhost:8082/strict_test 

log: 2019/03/07 16:03:13 Works
colm.anseo
la source
6
Merci d'avoir expliqué les opinions au lieu de simplement dire que quelque chose ne va pas
Fjolnir Dvorak
savez-vous ce qu'il ne gère pas? j'ai vu le test peut être en json deux fois et il accepte la 2ème occurrence
tooptoop4
@ tooptoop4, il faudrait écrire un décodeur personnalisé pour avertir des champs en double - ajoutant des inefficacités au décodeur - tout pour gérer un scénario qui ne se produirait jamais. Aucun encodeur JSON standard ne produirait jamais de champs en double.
colm.anseo
20

J'ai trouvé l'exemple suivant de la documentation vraiment utile (source ici ).

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "strings"
)

func main() {
    const jsonStream = `
        {"Name": "Ed", "Text": "Knock knock."}
        {"Name": "Sam", "Text": "Who's there?"}
        {"Name": "Ed", "Text": "Go fmt."}
        {"Name": "Sam", "Text": "Go fmt who?"}
        {"Name": "Ed", "Text": "Go fmt yourself!"}
    `
    type Message struct {
        Name, Text string
    }
    dec := json.NewDecoder(strings.NewReader(jsonStream))
    for {
        var m Message
        if err := dec.Decode(&m); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%s: %s\n", m.Name, m.Text)
    }
}

La clé ici étant que l'OP cherchait à décoder

type test_struct struct {
    Test string
}

... auquel cas nous supprimerions le const jsonStream, et remplacerions le Messagestruct par le test_struct:

func test(rw http.ResponseWriter, req *http.Request) {
    dec := json.NewDecoder(req.Body)
    for {
        var t test_struct
        if err := dec.Decode(&t); err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
        log.Printf("%s\n", t.Test)
    }
}

Mise à jour : j'ajouterais également que cet article fournit également d'excellentes données sur la réponse avec JSON. L'auteur explique struct tags, dont je n'étais pas au courant.

Étant donné que JSON ne ressemble pas normalement {"Test": "test", "SomeKey": "SomeVal"}, mais plutôt {"test": "test", "somekey": "some value"}, vous pouvez restructurer votre structure comme ceci:

type test_struct struct {
    Test string `json:"test"`
    SomeKey string `json:"some-key"`
}

... et maintenant votre gestionnaire analysera JSON en utilisant "une clé" par opposition à "une clé" (que vous utiliserez en interne).

JohnnyCoder
la source
1
type test struct {
    Test string `json:"test"`
}

func test(w http.ResponseWriter, req *http.Request) {
    var t test_struct

    body, _ := ioutil.ReadAll(req.Body)
    json.Unmarshal(body, &t)

    fmt.Println(t)
}
Angelica Payawal
la source