Unmarshaler for runtime type

main
Drew Bednar 1 month ago
parent d72ca6c064
commit c51b03de4f

@ -204,7 +204,7 @@ When you decode a JSON number into an any type the value will have the underlyin
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: 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:
``` ```golang
js := `10` js := `10`
var n any var n any
@ -251,3 +251,13 @@ Using the `errors.Is()` and `errors.As()` functions you can handle these errors
|-------------|------------------------------------------|----------------|-----------------------------------------| |-------------|------------------------------------------|----------------|-----------------------------------------|
| `errors.Is` | Compare error values (with wrapping) | Specific value | `errors.Is(err, io.EOF)` | | `errors.Is` | Compare error values (with wrapping) | Specific value | `errors.Is(err, io.EOF)` |
| `errors.As` | Check and extract a specific error type | Specific type | `errors.As(err, *json.SyntaxError)` | | `errors.As` | Check and extract a specific error type | Specific type | `errors.As(err, *json.SyntaxError)` |
#### Custom Decoding
If we want to perform some kind of custom decoding for our type, this can be done with the json.Unmarshaler interface
```golang
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
```

@ -18,10 +18,12 @@ func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Reques
// just like encoding the field names need to be exported to have Decode set them // just like encoding the field names need to be exported to have Decode set them
var input struct { var input struct {
Title string `json:"title"` Title string `json:"title"`
Year int32 `json:"year"` Year int32 `json:"year"`
Runtime int32 `json:"runtime"` // implemented an Unmarshaler receiver for this
Genres []string `json:"genres"` // Runtime int32 `json:"runtime"`
Runtime data.Runtime `json:"runtime"`
Genres []string `json:"genres"`
} }
// reads from the request body and decodes to our input struct // reads from the request body and decodes to our input struct
@ -61,9 +63,9 @@ func (app *application) getMovieHandler(w http.ResponseWriter, r *http.Request)
ID: id, ID: id,
CreatedAt: time.Now(), CreatedAt: time.Now(),
Title: "Eisenhorn", Title: "Eisenhorn",
Runtime: 102, // Runtime: 102,
Genres: []string{"sci-fi", "action"}, Genres: []string{"sci-fi", "action"},
Version: 1, Version: 1,
} }
app.logger.Info("Hit the get movies and found", "movie", movie.Title) app.logger.Info("Hit the get movies and found", "movie", movie.Title)

@ -49,7 +49,7 @@ func TestHealthRoute(t *testing.T) {
func TestCreateMovieHandler(t *testing.T) { func TestCreateMovieHandler(t *testing.T) {
respRec := httptest.NewRecorder() respRec := httptest.NewRecorder()
requestBody := `{"title": "Moana", "year": 2019, "runtime": 120, "genres": ["family", "Samoan"]}` requestBody := `{"title": "Moana", "year": 2019, "runtime": "120 mins", "genres": ["family", "Samoan"]}`
r, err := http.NewRequest(http.MethodPost, "/v1/movies", strings.NewReader(requestBody)) r, err := http.NewRequest(http.MethodPost, "/v1/movies", strings.NewReader(requestBody))
if err != nil { if err != nil {
@ -114,7 +114,7 @@ func TestCreateMovieError(t *testing.T) {
}, },
{ {
name: "Send unknown field", name: "Send unknown field",
input: strings.NewReader(`{"title": "Moana", "year": 2019, "runtime": 120, "genres": ["family", "Samoan"], "rating": "PG"}`), input: strings.NewReader(`{"title": "Moana", "year": 2019, "runtime": "120 mins", "genres": ["family", "Samoan"], "rating": "PG"}`),
wantBody: "body contains unknown key", wantBody: "body contains unknown key",
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
@ -131,6 +131,12 @@ func TestCreateMovieError(t *testing.T) {
wantBody: "body must not be larger than 1048576 bytes", wantBody: "body must not be larger than 1048576 bytes",
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
{
name: "Send invalid runtime",
input: strings.NewReader(`{"title": "Moana", "runtime": 120}`),
wantBody: "invalid runtime format",
wantCode: http.StatusBadRequest,
},
} }
for _, test := range tests { for _, test := range tests {

@ -1,10 +1,16 @@
package data package data
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings"
) )
// Define an error that our UnmarshalJSON() method can return if we're unable to parse
// or convert the JSON string successfully.
var ErrInvalidRuntimeFormat = errors.New("invalid runtime format")
// Declare a custom Runtime type, which has the underlying int32 type // Declare a custom Runtime type, which has the underlying int32 type
type Runtime int32 type Runtime int32
@ -25,3 +31,42 @@ func (r Runtime) MarshalJSON() ([]byte, error) {
return []byte(quotedJSONValue), nil return []byte(quotedJSONValue), nil
} }
// Implement a UnmarshalJSON() method on the Runtime type so that it satisfies the
// json.Unmarshaler interface. IMPORTANT: Because UnmarshalJSON() needs to modify the
// receiver (our Runtime type), we must use a pointer receiver for this to work
// correctly. Otherwise, we will only be modifying a copy (which is then discarded when
// this method returns).
func (r *Runtime) UnmarshalJSON(jsonValue []byte) error {
// We expect that the incoming JSON value will be a string in the format
// "<runtime> mins", and the first thing we need to do is remove the surrounding
// double-quotes from this string. If we can't unquote it, then we return the
// ErrInvalidRuntimeFormat error.
unquotedJSONValue, err := strconv.Unquote(string(jsonValue))
if err != nil {
return ErrInvalidRuntimeFormat
}
// Split the string to isolate the part containing the number.
parts := strings.Split(unquotedJSONValue, " ")
// Sanity check the parts of the string to make sure it was in the expected format.
// If it isn't, we return the ErrInvalidRuntimeFormat error again.
if len(parts) != 2 || parts[1] != "mins" {
return ErrInvalidRuntimeFormat
}
// Otherwise, parse the string containing the number into an int32. Again, if this
// fails return the ErrInvalidRuntimeFormat error.
i, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return ErrInvalidRuntimeFormat
}
// Convert the int32 to a Runtime type and assign this to the receiver. Note that we
// use the * operator to deference the receiver (which is a pointer to a Runtime
// type) in order to set the underlying value of the pointer.
*r = Runtime(i)
return nil
}

Loading…
Cancel
Save