swaggest / rest

Web services with OpenAPI and JSON Schema done quick in Go
https://pkg.go.dev/github.com/swaggest/rest
MIT License
375 stars 17 forks source link

Struct tags get ignored for net.Prefix #161

Closed steffann closed 1 year ago

steffann commented 1 year ago

Describe the bug The tags on netip.Prefix types get ignored when generating the OpenAPI spec.

To Reproduce Here is a full example:

package main

import (
    "context"
    "net/http"
    "net/netip"

    "github.com/swaggest/rest/web"
    "github.com/swaggest/swgui/v4emb"
    "github.com/swaggest/usecase"
)

type Prefix struct {
    ID     uint64       `json:"id" readonly:"yes"`
    Name   string       `json:"name" required:"true" example:"My Name" description:"Some human-readable name"`
    Owner  string       `json:"owner" example:"sander@example.com" format:"idn-email"`
    Prefix netip.Prefix `json:"prefix" required:"true" example:"192.168.0.0/24" description:"Prefix in CIDR notation" format:"cidr"`
}

func getPrefixes() usecase.Interactor {
    u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *[]Prefix) error {
        *output = append(*output, Prefix{
            ID:     1,
            Name:   "Example name",
            Owner:  "sander@example.net",
            Prefix: netip.MustParsePrefix("10.0.0.0/8"),
        })
        return nil
    })
    return u
}

func main() {
    service := web.DefaultService()

    service.Get("/prefixes", getPrefixes())
    service.Docs("/docs", v4emb.New)

    if err := http.ListenAndServe("localhost:8080", service); err != nil {
        panic(err)
    }
}

When looking at the schema returned by http://localhost:8080/docs/openapi.json I see:

{
  "…",
  "components": {
    "schemas": {
      "NetipPrefix": {
        "type": "string"
      },
      "Prefix": {
        "required": [
          "name",
          "prefix"
        ],
        "type": "object",
        "properties": {
          "id": {
            "minimum": 0,
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Some human-readable name",
            "example": "My Name"
          },
          "owner": {
            "type": "string",
            "format": "idn-email",
            "example": "sander@example.com"
          },
          "prefix": {
            "$ref": "#/components/schemas/NetipPrefix"
          }
        }
      }
    }
  }
}

Expected behaviour I expected to see my example, description and format tags represented in the schema output.

I also didn't expect that the netip.Prefix would be represented as

"NetipPrefix": {
  "type": "string"
}

but that's just an implementation detail I guess.

My preferred output would be to have the prefix field documented as

"prefix": {
  "type": "string",
  "format": "cidr",
  "description": "Prefix in CIDR notation",
  "example": "sander@example.com"
}
vearutop commented 1 year ago

This is a somewhat tricky issue.

Explanation of current behavior:

And here is the problem.

Struct fields can't go into referenced schema, because potentially there can be multiple conflicting definitions, e.g.

type Prefix struct {
    Prefix1 netip.Prefix `json:"prefix1" required:"true" example:"192.168.0.0/24" description:"Prefix in CIDR notation" format:"cidr"`
    Prefix2 netip.Prefix `json:"prefix2" required:"true" example:"foo" description:"bar" format:"baz"`
}

The schema could be inlined (without a $ref), but that is also not the best general solution because it

A "clean" general solution might be leveraging allOf to combine both $ref and struct field keywords, but it also leads to extra complexity in schema.

A current approach (workaround) to handle such case is to declare centralized schema for a 3rd-party named type (instead of struct tags), like here.

    // Create custom schema mapping for 3rd party type.
    uuidDef := jsonschema.Schema{}
    uuidDef.AddType(jsonschema.String)
    uuidDef.WithFormat("uuid")
    uuidDef.WithExamples("248df4b7-aa70-47b8-a036-33ac447e668d")
    s.OpenAPICollector.Reflector().AddTypeMapping(uuid.UUID{}, uuidDef)

For this particular case, I think we can introduce a special case to not use $ref (always inline) for named types that have trivial schema (type only, e.g. {"type":"string"}).

steffann commented 1 year ago

Thanks for looking into this! I like the simplicity of not creating a named type for just {"type":"string"}. To me that also makes the spec easier to read.