Manejo de solicitud de publicación JSON en Go

250

Así que tengo lo siguiente, que parece increíblemente hacky, y he estado pensando que Go tiene bibliotecas mejor diseñadas que esta, pero no puedo encontrar un ejemplo de Go manejando una solicitud POST de datos JSON. Todos son POST de forma.

Aquí hay un ejemplo de solicitud: curl -X POST -d "{\"test\": \"that\"}" http://localhost:8082/test

Y aquí está el código, con los registros incrustados:

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

Tiene que haber una mejor manera, ¿verdad? Estoy perplejo al encontrar cuál podría ser la mejor práctica.

(Go también se conoce como Golang para los motores de búsqueda, y se menciona aquí para que otros puedan encontrarlo).

TomJ
fuente
3
si lo usa curl -X POST -H 'Content-Type: application/json' -d "{\"test\": \"that\"}", entonces req.Form["test"]debería regresar"that"
Vinicius
@Vinicius, ¿hay alguna prueba de esto?
diralik

Respuestas:

388

Por favor, use en json.Decoderlugar 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
fuente
79
¿Podría explicar por qué?
Ryan Bigg
86
Para empezar, parece que esto puede manejar una secuencia en lugar de necesitar que lo cargue todo en un búfer usted mismo. (Soy un BTW diferente)
Joe
77
Me pregunto cómo sería el manejo adecuado de errores en este caso. No creo que sea una buena idea entrar en pánico ante un json no válido.
codepushr
15
No creo que necesite defer req.Body.Close()De los documentos: "El servidor cerrará el cuerpo de la solicitud. El controlador ServeHTTP no necesita". También para responder a @thisisnotabus, de los documentos: "Para las solicitudes del servidor, el cuerpo de la solicitud siempre es nulo, pero devolverá EOF inmediatamente cuando no hay ningún cuerpo presente" golang.org/pkg/net/http/#Request
Drew LeSueur
22
Sugeriría no usar json.Decoder. Está destinado a secuencias de objetos JSON, no a un solo objeto. No es más eficiente para un solo objeto JSON ya que lee todo el objeto en la memoria. Tiene el inconveniente de que si se incluye basura después del objeto, no se quejará. Dependiendo de algunos factores, es json.Decoderposible que no lea completamente el cuerpo y la conexión no sea elegible para su reutilización.
Kale B
85

Necesitas leer de req.Body. El ParseFormmétodo lee desde req.Bodyy luego lo analiza en formato estándar codificado HTTP. Lo que desea es leer el cuerpo y analizarlo en formato JSON.

Aquí está tu código actualizado.

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
fuente
¡Gracias! Ahora veo a dónde iba mal. Si llama req.ParseForm(), lo que estaba haciendo en intentos anteriores de tratar de resolver este problema, antes de intentar leerlo req.Body, parece despejar el cuerpo y unexpected end of JSON inputse arroja cuando va a Unmarshal(al menos en 1.0.2)
TomJ
1
@Daniel: Cuando hago curl -X POST -d "{\" tes \ ": \" that \ "}" localhost: 8082 / test , log.Println (t.Test) vuelve vacío. Por qué ? O para el caso, si publica cualquier otro JSON, vuelve vacío
Somesh
Su solicitud POST es incorrecta. tes! = prueba. Aprecio que fue hace 5 años: /
Rambatino
Este es un buen ejemplo simple!
15412s
Este es un buen consejo, pero para ser claros, las respuestas que se refieren al uso de json.NewDecoder(req.Body)también son correctas.
Rico
59

Me estaba volviendo loco con este problema exacto. Mi JSON Marshaller y Unmarshaller no poblaban mi estructura Go. Luego encontré la solución en https://eager.io/blog/go-and-json :

"Como con todas las estructuras en Go, es importante recordar que solo los campos con una primera letra mayúscula son visibles para programas externos como el JSON Marshaller".

¡Después de eso, mi Marshaller y Unmarshaller funcionaron perfectamente!

Steve Stilson
fuente
Incluya algunos fragmentos del enlace. Si queda en desuso, los ejemplos se perderán.
030
47

Hay dos razones por las json.Decoderque se debe preferir a las json.Unmarshalque no se abordan en la respuesta más popular de 2013:

  1. Febrero de 2018, go 1.10introdujo un nuevo método json.Decoder.DisallowUnknownFields () que aborda la preocupación de detectar entradas JSON no deseadas
  2. req.Bodyya es un io.Reader. Leer todo su contenido y luego json.Unmarshaldesperdiciar recursos si la transmisión fue, digamos un bloque de 10 MB de JSON no válido. Analizar el cuerpo de la solicitud, con json.Decoder, a medida que se transmite , provocaría un error de análisis temprano si se encuentra un JSON no válido. Procesamiento de E / S flujos en tiempo real es el preferido ir vías .

Abordar algunos de los comentarios de los usuarios sobre la detección de entradas incorrectas del usuario:

Para hacer cumplir los campos obligatorios y otras comprobaciones de saneamiento, intente:

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)

Patio de recreo

Salida típica:

$ 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
fuente
66
Gracias por explicar las opiniones en lugar de simplemente decir que algo es malo
Fjolnir Dvorak
sabes lo que no maneja? Vi que Test puede estar en json dos veces y acepta la segunda aparición
tooptoop4
@ tooptoop4 uno necesitaría escribir un decodificador personalizado para advertir sobre campos duplicados, agregando ineficiencias al decodificador, todo para manejar un escenario que nunca sucedería. Ningún codificador JSON estándar produciría campos duplicados.
colm.anseo
20

Encontré el siguiente ejemplo de los documentos realmente útil (fuente aquí ).

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 clave aquí es que el OP estaba buscando decodificar

type test_struct struct {
    Test string
}

... en cuyo caso soltaríamos const jsonStream, y reemplazaríamos la Messageestructura con 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)
    }
}

Actualización : también agregaría que esta publicación también proporciona excelentes datos sobre cómo responder con JSON. El autor explica struct tags, de lo que no estaba al tanto.

Dado que JSON normalmente no se ve así {"Test": "test", "SomeKey": "SomeVal"}, sino {"test": "test", "somekey": "some value"}que puede reestructurar su estructura de esta manera:

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

... y ahora su controlador analizará JSON usando "alguna clave" en lugar de "SomeKey" (que utilizará internamente).

JohnnyCoder
fuente
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)
}
Angélica Payawal
fuente