Adding notes on json decoding

main
Drew Bednar 19 hours ago
parent c726f59b6e
commit 1ab188c7b2

@ -3,4 +3,8 @@
[build]
kill_delay = "1s"
send_interrupt = true
```
```
## Creating a movie
curl -i -X POST -d '{"title":"Moana","year":2016,"runtime":107, "genres":["animation","adventure"]}' http://0.0.0.0:5002/v1/movies

@ -62,7 +62,7 @@ When Go is encoding a particular type to JSON, it looks to see if the type has a
Here is the interface
```
```golang
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
@ -94,3 +94,143 @@ Enveloping response data like this isnt strictly necessary, and whether you c
1. Including a key name (like "movie") at the top-level of the JSON helps make the response more self-documenting. For any humans who see the response out of context, it is a bit easier to understand what the data relates to.
2. It reduces the risk of errors on the client side, because its harder to accidentally process one response thinking that it is something different. To get at the data, a client must explicitly reference it via the "movie" key.
3. If we always envelope the data returned by our API, then we mitigate a security vulnerability in older browsers which can arise if you return a JSON array as a response.
## Decoding JSON
The `json.Unmarshal()` is an alternative, but less performant and more verbose method of decoding json compared to `json.NewDecoder().Decode()`. It is preferable to use the `Decode` implementation.
```golang
var myanonymous struct {
Title string `json:"title`
Year int32 `json:"year"`
}
err := json.NewDecoder(r.Body).Decode(&myanonymous)
```
- When calling Decode() you must pass a non-nil pointer as the target decode destination. If you dont use a pointer, it will return a json.InvalidUnmarshalError error at runtime.
- If the target decode destination is a struct — like in our case — the struct fields must be exported (start with a capital letter). Just like with encoding, they need to be exported so that theyre visible to the encoding/json package.
- When decoding a JSON object into a struct, the key/value pairs in the JSON are mapped to the struct fields based on the struct tag names. If there is no matching struct tag, Go will attempt to decode the value into a field that matches the key name (exact matches are preferred, but it will fall back to a case-insensitive match). Any JSON key/value pairs which cannot be successfully mapped to the struct fields will be silently ignored.
- There is no need to close r.Body after it has been read. This will be done automatically by Gos http.Server, so you dont have to.
### Go decoding mapping reference
| Go type | JSON type |
|-----------------------------------------------|----------------------------|
| bool | JSON boolean |
| string | JSON string |
| int*, uint*, float*, rune | JSON number |
| arrays, non-nil slices | JSON array |
| structs, non-nil maps | JSON object |
| nil pointers, (interface values, slices, maps)| JSON null |
A note on null and decoding. If the struct we are decoding to contains pointers, then the value of null will be mapped to a nil pointer. In a non-pointer struct the value will be the zero value of the type.
```golang
type Person struct {
Name *string `json:"name"` // nil if null
Age *int `json:"age"` // nil if null
}
```
vs
```golang
type Person struct {
Name string `json:"name"` // "" if null
Age int `json:"age"` // 0 if null
}
```
### JSON Decoding Nuances
#### Decoding into Go arrays
When youre decoding a JSON array into a Go array (not a slice) there are a couple of important behaviors to be aware of:
- If the Go array is smaller than the JSON array, then the additional JSON array elements are silently discarded.
- If the Go array is larger than the JSON array, then the additional Go array elements are set to their zero values.
#### Partial JSON decoding
If you have a lot of JSON input to process and only need a small part of it, its often possible to leverage the json.RawMessage type to help deal with this. For example:
```golang
// Let's say that the only thing we're interested in is processing the "genres" array in
// the following JSON object
js := `{"title": "Top Gun", "genres": ["action", "romance"], "year": 1986}`
// Decode the JSON object to a map[string]json.RawMessage type. The json.RawMessage
// values in the map will retain their original, un-decoded, JSON values.
var m map[string]json.RawMessage
err := json.NewDecoder(strings.NewReader(js)).Decode(&m)
if err != nil {
log.Fatal(err)
}
// We can then access the JSON "genres" value from the map and decode it as normal using
// the json.Unmarshal() function.
var genres []string
err = json.Unmarshal(m["genres"], &genres)
if err != nil {
log.Fatal(err)
}
fmt.Printf("genres: %v\n", genres)
```
#### Decoding into any types
Its possible to decode JSON values into an `any` type. When you do this, the underlying value that the `any` type holds will depend on the type of the JSON value being decoded.
| JSON type | Go type |
|---------------------------|-----------------------------------------------|
| JSON boolean | bool |
| JSON string | string |
| JSON number | float64 |
| JSON array | []any |
| JSON object | map[string]any |
| JSON null | nil |
Decoding into an any type can be useful in situations where:
- You dont know in advance exactly what youre decoding.
- You need to decode JSON arrays which contain items with different JSON types.
- The key/value pair in a JSON object doesnt always contain values with the same JSON type.
When you decode a JSON number into an any type the value will have the underlying type `float64` — even if it is an integer in the original JSON. If you want to get the value as an integer (instead of a `float64`) you should call the UseNumber() method on your `json.Decoder` instance before decoding. This will cause all JSON numbers to be decoded to the underlying type `json.Number` instead of `float64`.
The `json.Number` type then provides an `Int64()` method that you can call to get the number as an `int64`, or the `String()` method to get the number as a `string`. For example:
```
js := `10`
var n any
dec := json.NewDecoder(strings.NewReader(js))
dec.UseNumber() // Call the UseNumber() method on the decoder before using it.
err := dec.Decode(&n)
if err != nil {
log.Fatal(err)
}
// Type assert the any value to a json.Number, and then call the Int64() method
// to get the number as a Go int64.
nInt64, err := n.(json.Number).Int64()
if err != nil {
log.Fatal(err)
}
// Likewise, you can use the String() method to get the number as a Go string.
nString := n.(json.Number).String()
fmt.Printf("type: %T; value: %v\n", n, n)
fmt.Printf("type: %T; value: %v\n", nInt64, nInt64)
fmt.Printf("type: %T; value: %v\n", nString, nString)
```
#### Struct tag directives
Using the struct tag `json:"-"` on a struct field will cause it to be ignored when decoding JSON, even if the JSON input contains a corresponding key/value pair. The `omitzero` and `omitempty` struct tag directives do not have any effect on JSON decoding behavior.

@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
@ -11,8 +12,33 @@ import (
)
func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted)
fmt.Fprintln(w, "Resource created")
// Declare an anonymous struct to hold the information that we expect to be in the
// HTTP request body (note that the field names and types in the struct are a subset
// of the Movie struct that we created earlier). This struct will be our *target
// decode destination*.
// just like encoding the field names need to be exported to have Decode set them
var input struct {
Title string `json:"title"`
Year int32 `json:"year"`
Runtime int32 `json:"runtime"`
Genres []string `json:"genres"`
}
// reads from the request body and decodes to our input struct
// must be a non-nil pointer. Otherwise will raise json.InvalidUnmarshalError
err := json.NewDecoder(r.Body).Decode(&input)
if err != nil {
app.errorResponse(w, r, http.StatusBadRequest, err.Error())
return
}
// TODO save to DB
// Dump the contents of the input struct in a HTTP response
// +v is the default format value plus field names for structs
fmt.Fprintf(w, "%+v\n", input)
}
func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request) {

Loading…
Cancel
Save