GithubHelp home page GithubHelp logo

danielgtaylor / huma Goto Github PK

View Code? Open in Web Editor NEW
1.3K 29.0 103.0 10.46 MB

Huma REST/HTTP API Framework for Golang with OpenAPI 3.1

Home Page: https://huma.rocks/

License: MIT License

Go 99.99% Lua 0.01%
golang-library openapi3 json-schema documentation openapi-server api rest web framework swagger-ui

huma's Introduction

Huma Logo

HUMA Powered CI codecov Docs Go Report Card

πŸŒŽδΈ­ζ–‡ζ–‡ζ‘£

A modern, simple, fast & flexible micro framework for building HTTP REST/RPC APIs in Go backed by OpenAPI 3 and JSON Schema. Pronounced IPA: /'hjuːmΙ‘/. The goals of this project are to provide:

  • Incremental adoption for teams with existing services
    • Bring your own router (including Go 1.22+), middleware, and logging/metrics
    • Extensible OpenAPI & JSON Schema layer to document existing routes
  • A modern REST or HTTP RPC API backend framework for Go developers
  • Guard rails to prevent common mistakes
  • Documentation that can't get out of date
  • High-quality generated developer tooling

Features include:

  • Declarative interface on top of your router of choice:
    • Operation & model documentation
    • Request params (path, query, header, or cookie)
    • Request body
    • Responses (including errors)
    • Response headers
  • JSON Errors using RFC9457 and application/problem+json by default (but can be changed)
  • Per-operation request size limits with sane defaults
  • Content negotiation between server and client
    • Support for JSON (RFC 8259) and optionally CBOR (RFC 7049) content types via the Accept header with the default config.
  • Conditional requests support, e.g. If-Match or If-Unmodified-Since header utilities.
  • Optional automatic generation of PATCH operations that support:
  • Annotated Go types for input and output models
    • Generates JSON Schema from Go types
    • Static typing for path/query/header params, bodies, response headers, etc.
    • Automatic input model validation & error handling
  • Documentation generation using Stoplight Elements
  • Optional CLI built-in, configured via arguments or environment variables
    • Set via e.g. -p 8000, --port=8000, or SERVICE_PORT=8000
    • Startup actions & graceful shutdown built-in
  • Generates OpenAPI for access to a rich ecosystem of tools
  • Generates JSON Schema for each resource using optional describedby link relation headers as well as optional $schema properties in returned objects that integrate into editors for validation & completion.

This project was inspired by FastAPI. Logo & branding designed by Kari Taylor.

Sponsors

A big thank you to our current & former sponsors:

Testimonials

This is by far my favorite web framework for Go. It is inspired by FastAPI, which is also amazing, and conforms to many RFCs for common web things ... I really like the feature set, the fact that it [can use] Chi, and the fact that it is still somehow relatively simple to use. I've tried other frameworks and they do not spark joy for me. - Jeb_Jenky

After working with #Golang for over a year, I stumbled upon Huma, the #FastAPI-inspired web framework. It’s the Christmas miracle I’ve been hoping for! This framework has everything! - Hana Mohan

I love Huma. Thank you, sincerely, for this awesome package. I’ve been using it for some time now and it’s been great! - plscott

Thank you Daniel for Huma. Superbly useful project and saves us a lot of time and hassle thanks to the OpenAPI gen β€” similar to FastAPI in Python. - WolvesOfAllStreets

Huma is wonderful, I've started working with it recently, and it's a pleasure, so thank you very much for your efforts πŸ™ - callmemicah

Install

Install via go get. Note that Go 1.20 or newer is required.

# After: go mod init ...
go get -u github.com/danielgtaylor/huma/v2

Example

Here is a complete basic hello world example in Huma, that shows how to initialize a Huma app complete with CLI, declare a resource operation, and define its handler function.

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
	"github.com/danielgtaylor/huma/v2/humacli"
	"github.com/go-chi/chi/v5"

	_ "github.com/danielgtaylor/huma/v2/formats/cbor"
)

// Options for the CLI. Pass `--port` or set the `SERVICE_PORT` env var.
type Options struct {
	Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
	Body struct {
		Message string `json:"message" example:"Hello, world!" doc:"Greeting message"`
	}
}

func main() {
	// Create a CLI app which takes a port option.
	cli := humacli.New(func(hooks humacli.Hooks, options *Options) {
		// Create a new router & API
		router := chi.NewMux()
		api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

		// Add the operation handler to the API.
		huma.Get(api, "/greeting/{name}", func(ctx context.Context, input *struct{
			Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
		}) (*GreetingOutput, error) {
			resp := &GreetingOutput{}
			resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
			return resp, nil
		})

		// Tell the CLI how to start your router.
		hooks.OnStart(func() {
			http.ListenAndServe(fmt.Sprintf(":%d", options.Port), router)
		})
	})

	// Run the CLI. When passed no commands, it starts the server.
	cli.Run()
}

Tip

Replace chi.NewMux() β†’ http.NewServeMux() and humachi.New β†’ humago.New to use the standard library router from Go 1.22+. Just make sure your go.mod has go 1.22 or newer listed in it. Everything else stays the same! Switch whenever you are ready.

You can test it with go run greet.go (optionally pass --port to change the default) and make a sample request using Restish (or curl):

# Get the message from the server
$ restish :8888/greeting/world
HTTP/1.1 200 OK
...
{
	$schema: "http://localhost:8888/schemas/GreetingOutputBody.json",
	message: "Hello, world!"
}

Even though the example is tiny you can also see some generated documentation at http://localhost:8888/docs. The generated OpenAPI is available at http://localhost:8888/openapi.json or http://localhost:8888/openapi.yaml.

Check out the Huma tutorial for a step-by-step guide to get started.

Documentation

See the https://huma.rocks/ website for full documentation in a presentation that's easier to navigate and search then this README. You can find the source for the site in the docs directory of this repo.

Official Go package documentation can always be found at https://pkg.go.dev/github.com/danielgtaylor/huma/v2.

Articles & Mentions

Be sure to star the project if you find it useful!

Star History Chart

huma's People

Contributors

aorban-isp avatar bekabaz avatar costela avatar cptkirk avatar danielgtaylor avatar deo986 avatar dependabot[bot] avatar fishwaldo avatar hagemt avatar iwong-isp avatar jamelt avatar james-andrewsmith avatar jh125486 avatar jpkalbacher avatar lgarrett-isp avatar maximdanilchenko avatar mbrunner-isp avatar mt35-rs avatar mtiller avatar nickajacks1 avatar nstapelbroek avatar qdongxu avatar ross96d avatar sdil avatar sslotnick-isp avatar ssoroka avatar victoraugustolls avatar weiser avatar x-user avatar xarunoba avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

huma's Issues

Allow overriding http status codes from `Resolve` methods

I'm trying to handle the textbook case of getting the subject from a JWT token. I first reached for custom type, Uid, to be added to the input model. A type does the JWT parsing and validation in the Resolve method but then I wasn't able to find a way to set http response status codes from method. If the jwt is missing, I want to return an 401 Unauthorized instead of 400 Bad Request.

For now, I've opted to implement this as a middle ware that adds it to the Context which ideal as I'll have to add this middl ware at every endpoint configuration. It can't be a global middle ware to handle the cases of public endpoints.

I understand this is an edge case and most Resolve implementers only need 400 Bad Request and I can't think of any elegant way to add it to the API but...this is a gap in the API design and I thought it deserved a ticket.

Better support for offline

My use case is as follows:

I want to be able to generate an aggregated opeapi.yaml/json ( in 1 file)including its json schema.
Likewise, be able to generate aggregated jsonschema (in 1 file) and non aggregated ones (multiple files)

A cli or entrypoint that xan be called in a mai. Is needed.
=> be able to automate stuffs much more easily without pulling external tools
=> offline rendering of api specs

Suggestions : if possible provide a package/go module that can only do those things

Recommendation for Handler-specific Inline Middleware

I'm somewhat stumped on how to implement handler-specific (aka inline) middleware with Huma. My specific use case is for fine-grained role-based access control, where each endpoint may have a different RBAC profile.

Here's an example of what I mean (dumbed down for simplicity's sake). The code is using Chi, but the principle should be general enough.

package main

import (
    "net/http"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // In the real world, the role would obviously be determined from a JWT token,
            // using an authorization introspection or some such mechanism.
            role := "admin"

            if role != "admin" {
                w.WriteHeader(http.StatusForbidden)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

func main() {
    r := chi.NewRouter()
    r.Use(middleware.Logger)

    r.Get("/public", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("public"))
    })

    // Use chi.With() to apply middleware to a subrouter
    r.With(RequireRole("admin")).Get("/admin", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("admin"))
    })

   http.ListenAndServe(":3000", r)
}

I can't for the life of me figure out how to translate this to Huma, where endpoint registration is abstracted away from the actual router implementation.

My current workaround (which naturally always works) is to embed the authorization check in the body of the endpoint operation itself. However, I'm wondering if there's a more canonical way to do the same thing with middleware. I also suppose the greater question really is whether and how the concept of router composition (i.e. for Chi the likes of the .With(), .Route(), .Group(), .Mount() methods) is applicable in Huma context.

I concede that it's quite possible I'm barking up the wrong tree here because I just don't get itℒ️. Either way, any advise comes highly appreciated.

PS:thank you for the amazing framework!

String Array Input Inconsistency

If my handler has an input:

type Input struct {
    Names []string `query:"name"`
}

Then huma expects me to call my endpoint as GET /endpoint?name=name1,name2. However, the way the /docs page expects it to be called is GET /endpoint?name=name1&name=name2. Calling like this results in the handler being given []string{"name1"} as its input - missing the second occurrence of the query parameter. This inconsistency means that we cannot test this endpoint using the /docs page.

We would prefer to be able to use the same query parameter name multiple times in the URL to build the array, like the docs page tries to do. Please could you add support for this to Huma?

Panic when using interfaces for Dependency

package main

import (
	"context"
	"net/http"

	"github.com/danielgtaylor/huma"
)

type Server interface {
	Go() bool
}

type ServerImpl struct{}

func (_ ServerImpl) Go() bool {
	return true
}

func main() {
	test()
}

func test() {

	apiMoc := &ServerImpl{}
	var server Server = apiMoc
	r := huma.NewRouter("demo", "1.0.0", huma.ProdServer("http://localhost:8888"))
	bindRoutes(server, r)

}

func bindRoutes(server Server, r *huma.Router) {

	sourceAPI := huma.SimpleDependency(server)

	captures := r.Resource("/v1/captures").With(huma.ContextDependency())

	captures.With(
		huma.ResponseError(http.StatusInternalServerError, "Server failure"),
		sourceAPI).Get("demo",
		func(ctx context.Context, server Server) bool {
			return server.Go()
		},
	)
}

Get the panic panic: return type should be *main.ServerImpl but got main.Server: dependency invalid

Server Sent Events

I was looking for a way for huma resources to implement Server Sent Events. In that example, they see if the response writer conforms to the Flusher interface. If it does, there is no problem. But I've tried that with huma.Context and it does not conform to the Flusher interface. As a result, it isn't possible to flush the events as they are generated.

Any suggestions here?

Middleware Headers

There are cases (cors being a big one) where middleware will introduce headers that appear in a response. The current validation logic in huma sees these headers and complains because there are unexpected headers in the response. Now, I could add those headers in the description of every possible response. But this seems like an awful lot of repetition. So I'm wondering if there is some way, when registering middleware, to also indicate to huma that a given header should be expected in all responses.

Alternatively, make the validation less aggressive and have it only flag cases where an expected header is missing (rather than complaining when an unexpected header is present...which is what is happening here).

Or perhaps I've missed something?

Access to Request

I see that the huma.Context has the request contained in it. But it doesn't expose this. So, for example, if I want to handle content negotiation on my own (because huma's logic isn't sufficient), I cannot get access to the underlying request nor its headers (from what I can see).

Would you accept a PR that exposes the request via a method?

Panic Should Generate a 500 Status Code

When lines like this are triggered, they don't return a 500 Internal Server Error (and ideally an RFC 7807 problem report). Instead, I would expect the handler to recover and return a 500 error along with an RFC 7807 description of the problem.

I should add that it is very easy to trigger such panics as a result of various validation steps. It doesn't seem reasonable to the client to simply terminate the request without any kind of response (which is what happens now).

Calling middleware.SetLoggerInContext in Middlewares

Greetings!

Was curious about the notes in the Logging section of the docs.

It's noted that you can call things like middleware.SetLoggerInContext() in the following "locations":

  • Operations
  • Resolvers

What about accessing data/information that's being generated in another middleware?

For additional context, I've got a middleware that generates a traceID (similar to the OpenTracing middleware, but w/ OpenTelemetry instead). I'd like to have the traceID injected into any logging throughout that request.

I was able to successfully do this in an endpoint, just like the docs, but I'd rather not have to add the logic to each endpoint :)

Here is what I'm doing inside the OpenTelemetry middleware to attempt to inject the traceID:

...

reqLogger := middleware.GetLogger(r.Context())
otelLogger := reqLogger.With("traceID", traceID)
otelLogger.Info("Greetings from the middleware!")
middleware.SetLoggerInContext(huma.ContextFromRequest(w, r), otelLogger)

r = r.WithContext(reqCtx)
next.ServeHTTP(w, r)
...

Notice I have a log entry of "Greetings from the middleware!", which does get logged and correctly includes the traceID field.

The moment I do this from an endpoint:

func MyHandler(ctx huma.Context, input MyStruct) {
  logger := middleware.GetLogger(ctx)
  logger.Info("Let's get started!")
}

The operation logging no longer includes the traceID field.

Here's a screenshot of what is logged:

image

Stable OpenAPI property ordering

Currently Huma generates OpenAPI properties in a random order. A stable order would be preferred as it makes things like diffs much cleaner.

OpenAPI is generated via gabs but it doesn't seem to have ordering/sort options available, so another approach will need to be used. Maps in Go always have their keys sorted lexicographically when marshaled into JSON, so that could be used.

Duplicate Models

I just stumbled over this project. So far it looks great but I recognized one pretty 'ugly' point.

In case of using a model twice e.g. encapsulated inside a slice and standalone the schema/model is redefined as ...2.

Is there an option to avoid this?
image

Allow file upload limit to be defined by the user

When you want to access data from the request through MultipartForm, huma context exposed a function called GetMultipartForm that parses the MultipartForm and returns it. The issue is that we are limited to the predefined limit of 8*1024 as shown in the snippet below.

func (c *chiContext) GetMultipartForm() (*multipart.Form, error) {
	err := c.r.ParseMultipartForm(8 * 1024)
	return c.r.MultipartForm, err
}

Link to code snippet

Can we allow the user to set this limit in the config when initializing the huma router?

Operation Specific Security Policies

Currently, it appears that huma only provides the ability to specify the security scheme for the entire API via router.GatewayAuthCode(...) and router.SecurityRequirement(...). While OpenAPI 3.0.x allows for a security scheme to be specified per operation:

After you have defined the security schemes in the securitySchemes section, you can apply them to the whole API or individual operations by adding the security section on the root level or operation level, respectively

...it appears that huma only supports specifying the security scheme at the global level. This is substantiated by looking at the definition of the Operation struct (which seems to contain no fields capable of storing the security scheme information).

Is this all accurate or am I missing something?

Assuming this assessment is correct, then I'd like to suggest adding support for operation specific security schemes.

Invalid Use of $schema

Hi,
I'm looking at fixing the issue with DisableSchemaProperty not working... but think I've found another potential issue.

the $schema property is intended to identify the dialect of json-schema used, and not the actual schema for the json file we are presenting:

Simple Explanation:
https://www.tutorialspoint.com/json/json_schema.htm

From the actual specs:
https://json-schema.org/understanding-json-schema/reference/schema.html

This is why when copy/pasting the examples presented in the /docs UI, it fails validation if it includes the $schema property.

Schema: anyOf, allOf, oneOf, not

The OpenAPI 3.1 inherits the definition of a Schema Object from JSON Schema Draft 2020-12, which defines anyOf, allOf, oneOf, and not

All things considered, extending Validate and the Schema struct to work with this doesn't seem hugely invasive (albeit nontrivial).
What I see being most tricky is having a single endpoint that can accept more than one of bool, int, float, string, []SomeStruct and SomeStruct (my own use case involves a POST endpoint accepting both []SomeStruct and SomeStruct). Perhaps it would be possible to do something along the lines of what sse.Register does with the eventTypeMap?

Inline schemas still being generated with schema.GenerateInline = false

I'm working on a project and have run into issues generating schemas that include references to other schemas. Rather than trying to explain my schemas and structure, I used the bookstore example and was able to reproduce the issue without modifying the code.


Gist of the openapi.json file generated by the bookstore example with no modifications to the code

I ran into the first issue by just running the code without modification. Two Book schemas are generated (Book and Book2), Book has the $schema property and Book2 does not. Additionally, Book2 is used as the $ref schema for the BookList schema.

As shown in the next two schemas, adding app.DisableSchemaProperty() removed the Book2 schema but did not resolve the issue.


openapi.json after adding a call to app.DisableSchemaProperty()

Now, the real issue. The Genre.Books array uses an inline Book schema that is identical to the top-level Book schema. It's the same issue I'm having with my project. Structs used in other structs are generated inline rather than as references. I'm wondering if this has something to do with how the model is provided to the responses.OK().Model function because the BookList schema is properly generated as an array of the Book schema (using the $ref property). All the attached schemas include this issue.


openapi.json after adding app.DisableSchemaProperty() and schema.GenerateInline = false

As far as I can tell, the only difference between the 2nd and 3rd schema is that only the GenerateInline = false schema includes a ErrorDetail schema.

I'm not sure what to do here. I manually modified the openapi.json file to replace the incorrectly generated inline schemas with references to the correct schemas and it solved the issues I was having downstream when generating SDKs. I could continue with this manual solution, but I'm still developing the application and would have to repeat the process every time I make an update to the backend. Please let me know if you need me to provide more information about the issue.

Best practice to use other services (e.g. db client, s3 client) in route handler

I am looking for the best practices on how to use other services in route handlers (aka huma Operations).

In the past, I used the following approach with a chi router.

// Application object
type application struct {
	config         config
	db             *database.DB
	s3Client     *s3.S3
	logger       *slog.Logger
}

// Handler
func (app *application) getBooks(w http.ResponseWriter, r *http.Request){
   book, err := app.db.getBook(id)
...
}

// Router
mux := chi.NewRouter()
mux.Get("/books/{id}", app.getBook)

What's the best practice to use services like a database client or S3 client with huma?

There is one example project by the author of huma. In the example project the database service "booksMU" is a global object.

type APIServer struct{}

func (s *APIServer) RegisterGetBook(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "get-book",
		Method:      http.MethodGet,
		Path:        "/books/{book-id}",
		Tags:        []string{"Books"},
	}, func(ctx context.Context, input *struct {
		conditional.Params
		ID string `path:"book-id"`
	}) (*GetBookResponse, error) {
		booksMu.RLock()
		defer booksMu.RUnlock()
		...
		resp := &GetBookResponse{
			...
		}
		return resp, nil
	})
}

In my opinion in would be cleaner to attach global services like a database client or an S3 client to the APIServer object to make them available across all registered handlers.

What best practices do you follow and why?

How do you switch to v2?

I have tried go get github.com/danielgtaylor/huma@v2 but I get an error: no matching versions for query "v2". I think this is because when you ask for version "v2", go looks for tags since this looks like a version number, and errors when it doesn't find any. It doesn't bother looking for branches.

I can't figure out the correct replace directive to use. Please can you post what the replace directive you are using is?

Error handling with Server Sent Events

It is not clear to me how to indicate errors to the client when dealing with server sent events.

According to the documentation, you can only define what data to send but the status code will always be 200.

How can we indicate the client receiving the events that something went wrong?
Maybe an additional function called send.Error(msg, statusCode)?

For example something like this:

// Register using sse.Register instead of huma.Register
sse.Register(api, huma.Operation{
	OperationID: "sse",
	Method:      http.MethodGet,
	Path:        "/sse",
	Summary:     "Server sent events example",
}, map[string]any{
	// Mapping of event type name to Go struct for that event.
	"message":      DefaultMessage{},
	"userCreate":   UserCreatedEvent{},
	"mailRecieved": MailReceivedEvent{},
}, func(ctx context.Context, input *struct{}, send sse.Sender) {
	// Send an event every second for 10 seconds.
	
       // if db connection failed
      send.Error("db connection failed", 500)
})

Panic if adding example tag to body field which is a pointer

In order to distinguish between zero value and missing field, pointer is sometimes used in body fields. For example,

type InputBody struct {
    Quota *int64 `json:"quota" example:"1611198330"`
}

However, it throws an exception when adding example tag to the pointer field:

panic: unable to generate JSON schema: unable to convert float64 to *int64: schema is invalid

BTW, it works well when example tag is not added.

$schema in request body of input example

I noticed that the $schema property is present in the request body of my endpoint input:

schema

However, this causes an error when I click on "Try it" in the API documentation interface and send the example body without removing the $schema property first. The reason is that $schema is not expected by the API and so it throws an error.

IMHO it makes sense to have the schema somewhere in the API documentation but not as part of the example body of an input. It seems most relevant in the response data.

Production ready?

Hi guys.
Your project looks really cool.
Due to performance issues, I need to migrate an api in NodeJS (loopback) to Go, and in the search I found Huma, which has most of the things I need. However, I have only found references to the framework on a couple of sites.

Is the project ready to be used in production?
Are there long plans in its development?

I'm pretty new to golang, but if I can help with anything, let me know.

Potentially Nasty Ambiguity

@danielgtaylor in looking at the code, I see that you use t.Name() quite a bit. But the problem here is that if your API involves two types with the same name but from different packages, this will get horribly mangled. Just imagine a response of this type:

type MyModel struct {
  Employee employees.Record
  Company companies.Record
}

I know it is a bit contrived, but if it ever happened it would get really confusing because they'd both end up with the same schema (I'm pretty sure) under both the previous way of handling schemas (inlined) and the new way (with my changes to support recursion). You could create type aliases if you knew this was an issue, but that wouldn't address nested fields.

A bit part of the issue isn't so much the schemas themselves but the fact that they are addressed via URIs like #/components/schemas/<SchemaName> and /schemas/<SchemaName>.json. In those contexts, the use of t.Name() generates the ambiguity.

One option would be to use t.PkgPath(). This apparently generates a unique name across all packages. With the changes I made to the handler behind /schemas/<SchemaName>.json, it is no issue to include special characters either (i.e., it already handles generics). What do you think of the idea of using that instead? It might make the generated OpenAPI schemas a bit more noise (e.g., #/components/schemas/employees.Record, /schemas/employees.Record), but I think that is actually easier to read precisely because it lacks ambiguity (i.e., I don't wind up asking myself "which Record?).

I think this would be a simple change across the code base. I will attempt it and let you know the result.

Support mark as required for query parameters

Currently, the query parameters are optional in the generated openapi docuemntation. However, some parameters are required in the API, and they are not suitable for path.

It would be great if huma could support required for query parameters.

V2: Embedded structs are not inlined in the OpenAPI Spec

If I return the following type B as a body:

type A struct {
    A string `json:"a"`
}

type B struct {
    A
    B string `json:"b"`
}

The response json will look like:

{
  "a": "",
  "b": ""
}

Which is expected - this is how the encoding/json package works. However, the JSON Schema generated by Huma says that the response will be:

{
  "A": {
    "a": ""
  },
  "b": ""
}

This is wrong - when generating the schema, Huma should inline embedded structs.

ContentEncoding middleware should not write status if panic

ContentEncoding middleware always writes status code if Accept-Encoding is found in request header. However, it should not write status code when panic. Because it writes status code 200 OK before Recovery middleware writes 500 Internal Server Error. The response becomes 200 OK even when exception is found.

doc and description tags

Hi again !

Out of curiosity, is there a way to fill the doc and description attributes from code for a given field without including the whole text as a golang tag ?

I'm thinking of a syntax like:

//+ doc
// the doc field
//+ description
// the description field

and why not referencing a file from local filesystem ?
//+ file:=./markdowns/mydoc.md (where ./ is the relative path of the main.go being executed)

PUT and POST on humafiber always results in 400: Bad Request response

Simple POST and PUT endpoints do not seem to work with humafiber. I modified examples/cmd to use fiber to reproduce the issue:

$ restish put :3001/patch-test bar: sdfaf, foo: sdf
HTTP/1.1 400 Bad Request
Content-Length: 117
Content-Type: application/problem+cbor
Date: Tue, 12 Sep 2023 00:28:56 GMT
Link: </schemas/ErrorModel.json>; rel="describedBy"

{
  $schema: "http://localhost:3001/schemas/ErrorModel.json"
  detail: "request body is required"
  status: 400
  title: "Bad Request"
}

It appears that BodyReader() is returning a nil io.Reader despite the body bytes properly being received. I will diagnose this further tomorrow.

Value attached to request context in chi middleware is not available in context in huma operation

I am currently porting over a code from a pure chi router to huma + chi. I have a middleware that checks the Bearer token in the request header and fetches the user object from the db and then attaches it to the request context to make it available in all downstream functions. While porting the code over I ran into an issue. When I attach a value to the context of a request in a chi middleware, the value is not available in the context of the huma Operation.

My mental framework is as following:
Request -> chi middleware -> attaches value to Request context -> Request context is copied and made available in each huma Operation

That mental framework is not accurate. I'd like to understand where it breaks and how to best fix my issue of making a user object available within a huma Operation.

Code example below:

func (app *application) authenticate(next http.Handler) http.Handler {

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
               
                // simplified
	        user := getUser(r)
		r = contextSetAuthenticatedUser(r, user) // attach user to context of request!
		next.ServeHTTP(w, r)
	})

}


router := chi.NewMux()
router.Use(app.authenticate)
config := huma.DefaultConfig("Dumbledore API", "0.0.1")
api := humachi.New(router, config)


huma.Register(api, huma.Operation{
	OperationID: "dummy-route",
	Summary:     "dummy route",
	Method:      http.MethodGet,
	Path:        "/dummyroute",
}, func(ctx context.Context, i *struct{}) (*DummyRouteResponse, error) {

        // ISSUE: value is not available here
	user, ok := ctx.Value("user).(*database.User)
	if !ok {
		app.logger.Info("no authenticated user found in context")
		return nil, fmt.Errorf("no authenticated user found in context")
	}

	       ...
		
     })

return router
}

Validation of response

I noticed that there are ways to return results that have not been defined for an endpoint:

  • ❌ ctx.Write() seems to always get through and is not compared to what has been defined
  • ❌ ctx.WriteHeader() allows me to return a status code that has not been defined
  • ❌ ctx.Header().Set() allows me to change the content type to something that has not been defined
  • βœ… ctx.Header().Set() combined with ctx.WriteHeader() is validated and fails as expected when the result has not been defined
  • βœ… ctx.WriteModel() is validated and fails as expected when the result has not been defined
    • ❌ ctx.WriteModel() combined with ctx.Write() is not validated as pointed out above

What would be a good way of dealing with these inconsistencies? ctx.Write() sounds like a wildcard, documentation could cover its behavior. ctx.WriteHeader() should be verified. I'm not sure about ctx.Header().Set().

Validating JSON outside of a request

The validation is great, and it would be nice to be able to use it outside the context of a request (e.g., JSON file validation). However, the public interface is somewhat awkward due to the included optimizations.
Would it be in the spirit of things to add a simplified interface for generic schema validation? Or does supporting such a feature go out of the scope of Huma?

A few details that could potentially be abstracted away:

  • The Registry parameter is used only if the schema is a reference (or is a parent to one, recursively)
  • The PathBuffer could potentially be made optional
  • ValidateMode could have a default (or be ignored?)
  • ValidateResult could implement Error() and be returned as an error

I feel like a lot of the extra details could be abstracted away without giving up the optimizations.
For example, a config/option struct could be used that provides reasonable defaults.

Another approach might be to create a wrapper function that simplifies use (or even just suggest that users create one themselves).

Support for multiple response objects?

Howdy fellow Pacific Northwest person πŸ‘‹ ⛰️

I found huma while looking for something FastAPI-like but in Go, and wow, thanks so much for this project! This really fits my brain and I love not having to have a billion comments in order to get swaggo to build out OpenAPI spec for me.

I'm wondering how you feel about supporting multiple return types (anyOf, oneOf) (as supported in OpenAPI v3 linky)? It seems that this is not the most popular idea and looks like it got shot down in an issue on swaggo (also I guess they are supporting v2 only at this point?). Basically I have an API that I'd like to have return very slightly different payloads but I would really like the docs to show both (or however many) options that the endpoint could return.

I hacked around a bit and was able to get this output which is what I'm looking for more or less:

Screen Shot 2022-03-26 at 3 26 48 PM

Setting the oneOf models is handled like:

	note.Get("get-note", "Get a note by its ID",
		responses.OK().ModelsOneOf(NoteWrapper{}, []string{}, huma.ErrorModel{}),
		responses.NotFound(),
	).Run(func(ctx huma.Context, input NoteIDParam) {
		if n, ok := memoryDB.Load(input.NoteID); ok {
			// Note with that ID exists!
			ctx.WriteModel(http.StatusOK, n.(Note))
			return
		}

		ctx.WriteError(http.StatusNotFound, "Note "+input.NoteID+" not found")
	})

Obviously I added the ModlesOneOf method to Response.

Is this something you'd be interested in supporting? If not, I totally get it but figured I'd ask! If yes, I can get a PR cleaned up and raised shortly!

Thanks a bunch!

Carl

Migration to V2 using V1 Middleware and chi

Summary

When using the v1 middleware in v2 with chi as a router the application panics when accessing the /doc endpoint. Hence I am wondering if I missed something during the initialization of the middleware?

What I did

Following the documentation it is mentioned the v1 chi middleware is compatible to v2 and can be used accordingly.

🐳 Huma v1 middleware is compatible with Chi, so if you use that router with v2 you can continue to use the v1 middleware in a v2 application.

Given this I came up with this integration:

// related import
import (
	"github.com/danielgtaylor/huma/middleware"
	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
)

func main() {
...
	cli := huma.NewCLI(func(hooks huma.Hooks, opts *Options) {
		// Create a new router & API
		var api huma.API
		router := chi.NewMux()
		router.Use(middleware.DefaultChain)
		config := huma.DefaultConfig("My API", "1.0.0")
		api = humachi.New(router, config)
                 ...
	})

	// Run the CLI. When passed no commands, it starts the server.
	cli.Run()
}

Unfortunately the application crashes when accessing the /doc endpoint.

Error investigation

The panic occurs in the go-chi package when trying to access the context which is nil.
The context is access within the logging part of the middleware: middleware/logger.go.


Reproduction sample

The full example source code:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"git.i.mercedes-benz.com/cxp/transit-service-api-aws/pkg/types/response"

	"git.i.mercedes-benz.com/cxp/transit-service-api-aws/internal/billing"
	"git.i.mercedes-benz.com/cxp/transit-service-api-aws/internal/storage"
	"git.i.mercedes-benz.com/cxp/transit-service-api-aws/internal/storage/dummy"

	"github.com/danielgtaylor/huma/middleware"
	"github.com/danielgtaylor/huma/v2"
	"github.com/danielgtaylor/huma/v2/adapters/humachi"
	"github.com/go-chi/chi/v5"
	"github.com/spf13/cobra"
)

// Options for the CLI.
type Options struct {
	Port int `help:"Port to listen on" default:"8888"`
	Debug bool `doc:"Enable debug logging" default:"true"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
	Name string `path:"name" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
	Body struct {
		Message string `json:"message" doc:"Greeting message" example:"Hello, world!"`
	}
}

func main() {
	// Create a CLI app which takes a port option.
	cli := huma.NewCLI(func(hooks huma.Hooks, opts *Options) {
		// Create a new router & API
		var api huma.API
		router := chi.NewMux()
		router.Use(middleware.DefaultChain)
		config := huma.DefaultConfig("My API", "1.0.0")
		api = humachi.New(router, config)

		// Register GET /greeting/{name}
		huma.Register(api, huma.Operation{
			OperationID: "get-greeting",
			Summary:     "Get a greeting",
			Method:      http.MethodGet,
			Path:        "/greeting/{name}",
		}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
			resp := &GreetingOutput{}
			resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
			return resp, nil
		})

		srv := &http.Server{
			Addr:    fmt.Sprintf("%s:%d", "localhost", opts.Port),
			Handler: router,
		}

		// Tell the CLI how to start your router.
		hooks.OnStart(func() {
			// Start the server
			log.Printf("Server is running with: debug:%v host: %v port: %v\n", opts.Debug, "localhost", opts.Port)

			err := srv.ListenAndServe()
			if err != nil && err != http.ErrServerClosed {
				log.Fatalf("listen: %s\n", err)
			}
		})
	})

	// Run the CLI. When passed no commands, it starts the server.
	cli.Run()
}

Response Body can not be recognised in other directory and package

Versions:

  1. github.com/danielgtaylor/huma/v2 v2.1.0
  2. go 1.21.0

The my-app in the tutorial works fine.

after expand my-app, adding code all in main.go ( the main package), the open API spec works as expecteddemo code 1:

Response Example:

{
  "$schema": "http://example.com",
  "code": 0,
  "data": {
    "Greeting": "string",
    "Name": "string"
  },
  "msg": "string"
}

but after moving the expanded code to other directory and package ( the my-app/module/submodule package), the open API spec pages shows errors demo code 2 :

The Responses Section shows

Body application/json
No schema defined

Resonse Example:

Example cannot be created for this schema
Error: Invalid reference token: Responsemy-app

This is the first time I use huma. Is it a bug or any convention I did not follow? thanks for a kindly clarification.

Recursion in Schemas

I didn't see any notes in the docs about how to handle recursion in schemas. JSON Schema itself can handle this by using schema references. But huma doesn't seem to be able to recognize the recursion because if I have a model with recursion (with the recursive field marked as omitempty), huma crashes when accessing an endpoint associated with that model.

Errors have bogus schema

The 404 handler generates an RFC 7807 application/problem+json, but it doesn't point to a valid schema. Here is a sample 404 response:

{"$schema":"http://localhost:8888/ErrorModel.json","detail":"Cannot find /ErrorModel.json","status":404,"title":"Not Found"}

The $schema field should be http://localhost:8888/schemas/ErrorModel.json. Note the presence of /schemas in the path.

Support multiple request input sources

When a field has multiple request inputs sources, e.g. path and json, the library will throw an error.

As an example, when you have a field like User string `json:"user" path:"user"` , and you want to use it in 2 ways: in GET handler when user exists in URI as path param, and in POST handler when user is sent using JSON body. Struct with fields like this will result in error Parameter 'user' not in URI path.

I can implement this, but let me know if this fits in the library.

Autopatch and Readonly Fields?

I'm having a look at the new Autopatch functionality.

I send something like this:
[{"op":"replace","path":"/description","value":"MouthPieceApp123"}]

In my GET Operation, I return a model with a number of fields Marked ReadOnly, but description is not:

type appGetResponse struct {
	ID          int                  `doc:"ID of the Application" readOnly:"true" json:"id"`
	Name        string               `doc:"Name of the Application" pattern:"^[a-zA-Z0-9_]+$" minLength:"3" maxLength:"32" json:"name"`
	Description string               `doc:"Description of the Application" pattern:"^[a-zA-Z0-9_]+$" minLength:"0" maxLength:"255" json:"description,omitempty"`
	Filters     []appFiltersResponse `doc:"Filters of the Application" json:"filters,omitempty" readOnly:"true"`
	Groups      []appGroupResponse   `doc:"Groups of the Application" json:"groups,omitempty" readOnly:"true"`
}

and my PUT operation input model is:

type appPutRequest struct {
	ID   int `doc:"ID of the Application" readOnly:"true" path:"id"`
	Body struct {
		appGetResponse
	}
}

Before hitting the actual PUT Operation, Huma rejects a json-patch operation with errors such as:

{
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "Error while processing input parameters",
  "errors": [
    {
      "message": "Additional property id is not allowed",
      "location": "body",
      "value": 1
    },
    {
      "message": "Additional property groups is not allowed",
      "location": "body",
      "value": [
        {
          "Description": "Default MouthPiece App Group",
          "ID": 8589934593,
          "Name": "MouthPiece"
        }
      ]
    }
  ]
}

if I change the Body struct for my appPutRequest struct to just the mutable fields, I still get errors about additional fields - those copied from the internal GET request.

I'm guessing readonly fields should be dropped from the final request sent internally to the PUT handler, but looking over the patch code, I see we basically just copy the result of the GET operation into the json-patch library without any filtering at all. (

huma/patch.go

Line 225 in 1519e49

patched, err = patch.Apply(origWriter.Body.Bytes())
) so I'm guessing this usecase isn't handled?

Best practice for supporting multiple versions of the same API

Hi,

I am curious about best practices for supporting multiple versions of the same API.

I use chi as a router. Here are the docs on how the chi team suggestions to implement versioning.

I am not quite sure how this would translate to huma.

Assume we have a simple GET endpoint "/greeting". The route should return different values for different versions of the API. How would I implement that?

	router := chi.NewMux()
	api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

	// Register GET /greeting/{name}
	huma.Register(api, huma.Operation{
		OperationID: "get-greeting",
		Summary:     "Get a greeting",
		Method:      http.MethodGet,
		Path:        "/greeting/{name}",
	}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
		resp := &GreetingOutput{}
		resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
		return resp, nil
	})

	// Start the server!
	http.ListenAndServe("127.0.0.1:8888", router)

Happy holidays!

Route grouping/sub routing

Hello, I'm trying to migrate a project from chi to Huma + chi (nice work btw) and one problem that I'm finding is relative to using chi route grouping/sub routing.

I have something like this:

router.Group(func(r chi.Router) {
   r.Use(CustomMiddleware())
   RegisterHandler(r, http.MethodGet, "/public-route", a.PublicRouteHandler)
   // how handle the case below
   r.Route("/api", func(r chi.Router) {
   	r.Use(AuthenticatedMiddleware())
   	RegisterHandler(r, http.MethodPost, "/private-route-1", a.PrivateRoute1Handler)
   	r.Route("/sub-router-1", func(r chi.Router) {
   		RegisterHandler(r, http.MethodGet, "/private-sub-route-1", a.PrivateSubRoute1Handler)
   	})
   	r.Route("/sub-router-2", func(r chi.Router) {
   		RegisterHandler(r, http.MethodGet, "/private-sub-route-2", a.PrivateSubRoute2Handler)
   	})
   })
})

Now, the r.Use(CustomMiddleware()) is easy enough, I can just use humachi.New passing the returned r chi.Router after applying the middleware), registering the public route should be easy as well, now I'm not sure about the the first sub routing that adds the AuthenticatedMiddleware.

I dont really care about having to register the route as /api/sub-router-1 (instead of using a Route + /sub-router-1), but I'm not sure how apply this middleware only for a few routes.

I could adapt the middleware and wrap the handler with it (withAuthentication(a.PrivateRoute1Handler)), but maybe there is a easy/better way and I'm not seeing it.

Issue with Generics

If I have a model that is generic, then there seems to be an issue resolving the schemas. For example, if my model is of type Container[Thing] and I create my route with:

registry.Get("registry", "Get the registry",
		responses.OK().Model(Container[Thing]{})).Run(func(ctx huma.Context) {

		// The `WriteModel` convenience method handles content negotiation and
		// serializaing the response for you.
		entity := Container[Thing]{}
		entity.Properties = Thing{Name: "Test1"}

		ctx.WriteModel(http.StatusOK, entity)
	})

Then my response yields a Link header of </schemas/Container[main.Thing].json>; rel="describedby". By itself, I don't think this is an issue. But if I try to access the schema that that URL, I get 404 Not Found. Presumably this has something to do with the escaping of the characters in the URL or the pattern matching in the router. Not really sure, but it doesn't find it.

My current solution is to just alias the type in Go, e.g.,

type ContainerThing Container[Thing]

This seems to resolve the issue. But I'm not sure I'll have 100% control over such things in nested types.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.