How to generate OpenAPI v3 specification from Go source code?
Asked Answered
P

3

17

Is there a way to generate OpenAPI v3 specification from go source code? Let's say I have a go API like the one below and I'd like to generate the OpenAPI specification (yaml file) from it. Something similar to Python's Flask RESTX. I know there are tools that generate go source code from the specs, however, I'd like to do it the other way around.

package main

import "net/http"

func main() {
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("world\n"))
    })
    http.ListenAndServe(":5050", nil)
}
Polysaccharide answered 12/2, 2021 at 11:54 Comment(6)
It doesn't make much sense to first write an API implementation and then generate spec for it. Purpose of OpenAPI is exact opposite. Also such spec would be hardly complete (how such tool would know about e.g. auth headers handled by middleware or restrictions on input implemented in handler, etc.)Flatways
Totally agree with above statement. I always write the openapi YAML first & code second. Ensures the inputs & outputs across all routes are consistent especially when using schema $ref references.Submaxillary
OpenAPI as a spec does not suggest which way of creating it preferable. It all comes to particular tools and conventions. Wiring information about security definitions into API doc is definitely doable, please check github.com/swaggest/rest/blob/v0.1.18/_examples/task-api/… for example. And moreover, expressing spec from code is generally much less prone to human errors and outdatedness.Statius
I'm a vocal believer that using an OpenAPI spec generated from code as the authority on how to implement a client is generally a bad idea, akin to writing your UI/Functionality design docs after you've written your application. That said, there are absolutely reasons why you'd generate an OpenAPI spec from source code. This allows you to compare the generated spec against your designed spec to ensure it's compliant with the design.Barahona
There are two important aspects of API spec as seen by client. First, it has to be accurate. Second, it has to be backwards compatible in a reasonable interval. Neither of these properties are exclusively granted by spec first approach. Spec that is reflected from source has highest possible accuracy. Writing a spec by hand is a tedious process prone to errors and misalignment. In contrast, writing a zero implementation in Go is guarded by compile-time type safety and is ready for further actual implementation as soon as exported API spec is approved by peers.Statius
Goa will do it goa.design/learn/getting-startedRecommendation
S
12

You can employ github.com/swaggest/rest to build a self-documenting HTTP REST API. This library establishes a convention to declare handlers in a way that can be used to reflect documentation and schema and maintain a single source of truth about it.

In my personal opinion code first approach has advantages comparing to spec first approach. It can lower the entry bar by not requiring to be an expert in spec language syntax. And it may help to come up with a spec that is well balanced with implementation details.

With code first approach it is not necessary to implement a full service to get the spec. You only need to define the structures and interfaces and may postpone actual logic implementation.

Please check a brief usage example.

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/go-chi/chi"
    "github.com/go-chi/chi/middleware"
    "github.com/swaggest/rest"
    "github.com/swaggest/rest/chirouter"
    "github.com/swaggest/rest/jsonschema"
    "github.com/swaggest/rest/nethttp"
    "github.com/swaggest/rest/openapi"
    "github.com/swaggest/rest/request"
    "github.com/swaggest/rest/response"
    "github.com/swaggest/rest/response/gzip"
    "github.com/swaggest/swgui/v3cdn"
    "github.com/swaggest/usecase"
    "github.com/swaggest/usecase/status"
)

func main() {
    // Init API documentation schema.
    apiSchema := &openapi.Collector{}
    apiSchema.Reflector().SpecEns().Info.Title = "Basic Example"
    apiSchema.Reflector().SpecEns().Info.WithDescription("This app showcases a trivial REST API.")
    apiSchema.Reflector().SpecEns().Info.Version = "v1.2.3"

    // Setup request decoder and validator.
    validatorFactory := jsonschema.NewFactory(apiSchema, apiSchema)
    decoderFactory := request.NewDecoderFactory()
    decoderFactory.ApplyDefaults = true
    decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)

    // Create router.
    r := chirouter.NewWrapper(chi.NewRouter())

    // Setup middlewares.
    r.Use(
        middleware.Recoverer,                          // Panic recovery.
        nethttp.OpenAPIMiddleware(apiSchema),          // Documentation collector.
        request.DecoderMiddleware(decoderFactory),     // Request decoder setup.
        request.ValidatorMiddleware(validatorFactory), // Request validator setup.
        response.EncoderMiddleware,                    // Response encoder setup.
        gzip.Middleware,                               // Response compression with support for direct gzip pass through.
    )

    // Create use case interactor.
    u := usecase.IOInteractor{}

    // Describe use case interactor.
    u.SetTitle("Greeter")
    u.SetDescription("Greeter greets you.")

    // Declare input port type.
    type helloInput struct {
        Locale string `query:"locale" default:"en-US" pattern:"^[a-z]{2}-[A-Z]{2}$" enum:"ru-RU,en-US"`
        Name   string `path:"name" minLength:"3"` // Field tags define parameter location and JSON schema constraints.
    }
    u.Input = new(helloInput)

    // Declare output port type.
    type helloOutput struct {
        Now     time.Time `header:"X-Now" json:"-"`
        Message string    `json:"message"`
    }
    u.Output = new(helloOutput)

    u.SetExpectedErrors(status.InvalidArgument)
    messages := map[string]string{
        "en-US": "Hello, %s!",
        "ru-RU": "Привет, %s!",
    }
    u.Interactor = usecase.Interact(func(ctx context.Context, input, output interface{}) error {
        var (
            in  = input.(*helloInput)
            out = output.(*helloOutput)
        )

        msg, available := messages[in.Locale]
        if !available {
            return status.Wrap(errors.New("unknown locale"), status.InvalidArgument)
        }

        out.Message = fmt.Sprintf(msg, in.Name)
        out.Now = time.Now()

        return nil
    })

    // Add use case handler to router.
    r.Method(http.MethodGet, "/hello/{name}", nethttp.NewHandler(u))

    // Swagger UI endpoint at /docs.
    r.Method(http.MethodGet, "/docs/openapi.json", apiSchema)
    r.Mount("/docs", v3cdn.NewHandler(apiSchema.Reflector().Spec.Info.Title,
        "/docs/openapi.json", "/docs"))

    // Start server.
    log.Println("http://localhost:8011/docs")
    if err := http.ListenAndServe(":8011", r); err != nil {
        log.Fatal(err)
    }
}
Statius answered 13/2, 2021 at 9:54 Comment(0)
B
9

You can use Huma (full disclosure: I am the author) to generate OpenAPI 3.1 & JSON Schema from Go code, and it works with various popular routers so can be integrated into an existing codebase as needed. Huma makes use of Go generics to provide compile-time checks for input parameters, bodies, response headers, etc.

Here is a basic hello world example in just a few lines:

package main

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

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

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

// 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 new router & API
    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)
}

You can then easily get the generated OpenAPI from the service:

curl http://localhost:8888/openapi.json

You can also view docs generated from the OpenAPI at http://localhost:8888/docs.

Berkin answered 8/12, 2023 at 6:17 Comment(2)
What a beautiful library, I'm definitely checking it out, thanks!Women
Brilliant libraryPericlean
M
0

I think it should be possible, especially if the framework you are using allows you to walk all registered routes and extract the handler function, methods used, etc.

  1. Declare http handlers like this (or similar, the important thing is that request path/query/body parameters are sent into function with type information):
func setFeatureRequest(w http.ResponseWriter, r *http.Request, body *storage.FeatureRequest) {
    // Do something
}

func uploadBlob(w http.ResponseWriter, r *http.Request, blob []byte) {
    // Do something
}

func downloadProductFile(w http.ResponseWriter, r *http.Request, id int64) {
    // Do something
}
  1. Register them like this (or similar, the important thing is that the one registered has an underscore in front of it since it will be generated with go generate and contain the boilerplate necessary to call real handler):
router.HandleFunc("/feature-request", _setFeatureRequest).Methods("POST")
router.HandleFunc("/blob", _uploadBlob).Methods("POST")
router.HandleFunc("/product-file/{id}", _downloadProductFile).Methods("GET")
  1. Generate OpenAPI spec like this:
  • Walk the router and get the path, method, and handler

  • Strip the leading underscore from the handler name to get the function name

  • Use go/ast to extract the function signature metadata

  • Generate the OpenAPI spec from the function signature metadata

    • If parameter name is a path variable, set the field to "path"

    • Otherwise, set the field to "query" or "body" depending on the type of the parameter (struct = json body, otherwise query)

    • Query parameters should be pointers because they are optional (nil if not present)

    • Extract struct schema information using reflect package

    • If the parameter is bytes, set the format to "binary"

  1. Generate the server boilerplate (underscored handler function) using openapi spec
Millrace answered 21/5 at 18:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.