Documentation
¶
Overview ¶
Package request decodes a JSON request body into a typed value.
DecodeJSON reads exactly one JSON document into T:
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
input, err := request.DecodeJSON[createArticle](r)
It is strict by default, because a body a client got wrong should fail rather than apply in part:
- the media type must be application/json or application/*+json;
- an object field that T does not have is rejected, which catches typos; AllowUnknownFields turns this off;
- anything after the first document is rejected.
Errors ¶
A problem with what the client sent is a *DecodeError. Its DecodeErrorKind tells an empty body, an unsupported media type, malformed JSON, a value of the wrong type, an invalid value and several documents apart. For a value of the wrong type Path names the field, such as author.name. The cause is available through errors.Unwrap for logging and is not meant for the client.
Any other failure, such as an error reading the body, is returned as is. DecodeJSON imposes no size limit: wrap the body with http.MaxBytesReader, and its *http.MaxBytesError reaches the caller unchanged, so the client gets 413 rather than 400.
The problem package maps both to Problem Details.
Validation ¶
DecodeAndValidateJSON decodes the body and then passes *T to a Validator. The interface has a single method, so any validation library fits behind a one-line adapter. Its error is wrapped and stays available through errors.AsType.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DecodeAndValidateJSON ¶
DecodeAndValidateJSON decodes one JSON document and then validates the resulting *T. Decode errors and validation errors preserve their concrete types through errors.Unwrap.
Example ¶
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/uchaloop/httpx/request"
)
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
// titleValidator stands in for a validation library behind the Validator
// interface.
type titleValidator struct{}
func (titleValidator) Validate(value any) error {
if input, ok := value.(*createArticle); ok && len(strings.TrimSpace(input.Title)) == 0 {
return errors.New("title is a required field")
}
return nil
}
func jsonRequest(body string) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/articles", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
return r
}
func main() {
r := jsonRequest(`{"title":" ","status":"draft"}`)
_, err := request.DecodeAndValidateJSON[createArticle](r, titleValidator{})
fmt.Println(err)
}
Output: validate decoded JSON: title is a required field
func DecodeJSON ¶
func DecodeJSON[T any](r *http.Request, opts ...JSONOption) (T, error)
DecodeJSON decodes one required JSON document from r.Body into T.
The request must use application/json or an application/*+json media type. Unknown object fields and multiple JSON documents are rejected by default. DecodeJSON does not impose a body-size limit. A failure to read the body, such as an exceeded limit, is returned as is rather than as a DecodeError, so the limit's own status reaches the client.
Example ¶
package main
import (
"fmt"
"log"
"net/http"
"net/http/httptest"
"strings"
"github.com/uchaloop/httpx/request"
)
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
func main() {
r := httptest.NewRequest(
http.MethodPost,
"/articles",
strings.NewReader(`{"title":"HTTP boundaries","status":"draft"}`),
)
r.Header.Set("Content-Type", "application/json")
input, err := request.DecodeJSON[createArticle](r)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", input)
}
Output: {Title:HTTP boundaries Status:draft}
Example (BodyLimit) ¶
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/uchaloop/httpx/request"
)
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
func jsonRequest(body string) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/articles", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
return r
}
func main() {
r := jsonRequest(`{"title":"` + strings.Repeat("a", 64) + `"}`)
// DecodeJSON sets no limit, and the error of this one reaches the caller
// as is.
r.Body = http.MaxBytesReader(nil, r.Body, 32)
_, err := request.DecodeJSON[createArticle](r)
if tooLarge, ok := errors.AsType[*http.MaxBytesError](err); ok {
fmt.Println("body is over", tooLarge.Limit, "bytes")
}
}
Output: body is over 32 bytes
Types ¶
type DecodeError ¶
type DecodeError struct {
Kind DecodeErrorKind
Path string
// contains filtered or unexported fields
}
DecodeError describes a recoverable client input error. Cause remains available through errors.Unwrap for detailed mapping and logging.
Example ¶
package main
import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/uchaloop/httpx/request"
)
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
func jsonRequest(body string) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/articles", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
return r
}
func main() {
_, err := request.DecodeJSON[createArticle](jsonRequest(`{"title":5}`))
if decodeErr, ok := errors.AsType[*request.DecodeError](err); ok && decodeErr.Kind == request.DecodeErrorWrongType {
fmt.Println("wrong type at", decodeErr.Path)
}
}
Output: wrong type at title
func (*DecodeError) Error ¶
func (e *DecodeError) Error() string
func (*DecodeError) Unwrap ¶
func (e *DecodeError) Unwrap() error
Unwrap returns the underlying decoder or reader error.
type DecodeErrorKind ¶
type DecodeErrorKind uint8
DecodeErrorKind identifies a stable class of request JSON decoding failure.
const ( DecodeErrorEmptyBody DecodeErrorKind = iota + 1 DecodeErrorUnsupportedMediaType DecodeErrorMalformedJSON DecodeErrorWrongType DecodeErrorInvalidValue DecodeErrorMultipleValues )
type JSONOption ¶
type JSONOption func(*jsonOptions)
JSONOption changes DecodeJSON behavior.
func AllowUnknownFields ¶
func AllowUnknownFields() JSONOption
AllowUnknownFields allows object fields that are not present in the target Go type. DecodeJSON rejects them by default to catch client-side typos.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/uchaloop/httpx/request"
)
type createArticle struct {
Title string `json:"title"`
Status string `json:"status"`
}
func jsonRequest(body string) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/articles", strings.NewReader(body))
r.Header.Set("Content-Type", "application/json")
return r
}
func main() {
const body = `{"title":"HTTP boundaries","author":"Ada"}`
_, err := request.DecodeJSON[createArticle](jsonRequest(body))
fmt.Println(err)
input, err := request.DecodeJSON[createArticle](jsonRequest(body), request.AllowUnknownFields())
fmt.Println(input.Title, err)
}
Output: invalid JSON value: json: unknown field "author" HTTP boundaries <nil>