request

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 7 Imported by: 0

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

func DecodeAndValidateJSON[T any](
	r *http.Request,
	v Validator,
	opts ...JSONOption,
) (T, error)

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>

type Validator

type Validator interface {
	Validate(any) error
}

Validator describes the validation capability consumed by request helpers. Concrete validator adapters implement this interface outside the request package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL