Skip to content

Schema Customization#

Operation Schema#

Schemas that are generated for input/output bodies can be customized in a couple of different ways. First, when registering your operation you can provide your own request and/or response schemas if you want to override the entire body. The automatic generation only applies when you have not provided your own schema in the OpenAPI.

// Register an operation with a custom input body schema.
huma.Register(api, huma.Operation{
	OperationID: "my-operation",
	Method:      http.MethodPut,
	Path:        "/things/{thing-id}",
	Summary:     "Update a thing",
	RequestBody: &huma.RequestBody{
		Description: "My custom request schema",
		Content: map[string]*huma.MediaType{
			"application/json": {
				Schema: &huma.Schema{
					Type: 		 huma.TypeObject,
					Properties: map[string]*huma.Schema{
						"foo": {
							Type: huma.TypeString,
							Extensions: map[string]any{
								"x-custom-thing": "abc123",
}, func(ctx context.Context, input *MyInput) (*MyOutput, error) {
	// Implementation goes here...
	return nil, nil

Field Schema#

Second, this can be done on a per-field basis by making a struct that implements a special interface to get a schema, allowing you to e.g. encapsulate additional functionality within that field. This is the interface:

// SchemaProvider is an interface that can be implemented by types to provide
// a custom schema for themselves, overriding the built-in schema generation.
// This can be used by custom types with their own special serialization rules.
type SchemaProvider interface {
	Schema(r huma.Registry) *huma.Schema

The huma.Registry is passed to you and can be used to get schemas or refs for any embedded structs. Here is an example, where we want to know if a field was omitted vs. null vs. a value when sent as part of a request body. First we start by defininig the custom generic struct:

// OmittableNullable is a field which can be omitted from the input,
// set to `null`, or set to a value. Each state is tracked and can
// be checked for in handling code.
type OmittableNullable[T any] struct {
	Sent  bool
	Null  bool
	Value T

// UnmarshalJSON unmarshals this value from JSON input.
func (o *OmittableNullable[T]) UnmarshalJSON(b []byte) error {
	if len(b) > 0 {
		o.Sent = true
		if bytes.Equal(b, []byte("null")) {
			o.Null = true
			return nil
		return json.Unmarshal(b, &o.Value)
	return nil

// Schema returns a schema representing this value on the wire.
// It returns the schema of the contained type.
func (o OmittableNullable[T]) Schema(r huma.Registry) *huma.Schema {
	return r.Schema(reflect.TypeOf(o.Value), true, "")

This is how it can be used in an operation:

type MyResponse struct {
	Body struct {
		Message string `json:"message"`

huma.Register(api, huma.Operation{
	OperationID: "omittable",
	Method:      http.MethodPost,
	Path:        "/omittable",
	Summary:     "Omittable / nullable example",
}, func(ctx context.Context, input *struct {
	// Making the body a pointer makes it optional, as it may be `nil`.
	Body *struct {
		Name OmittableNullable[string] `json:"name,omitempty" maxLength:"10"`
}) (*MyResponse, error) {
	resp := &MyResponse{}
	if input.Body == nil {
		resp.Body.Message = "Body was not sent"
	} else if !input.Body.Name.Sent {
		resp.Body.Message = "Name was omitted from the request"
	} else if input.Body.Name.Null {
		resp.Body.Message = "Name was set to null"
	} else {
		resp.Body.Message = "Name was set to: " + input.Body.Name.Value
	return resp, nil

If you go to view the generated docs, you will see that the type of the name field is string and that it is optional, with a max length of 10, indicating that the custom schema was correctly used in place of one generated for the OmittableNullable[string] struct.

See for a full example along with how to call it. This just scratches the surface of what's possible with custom schemas for fields.

Dive Deeper#