huma icon indicating copy to clipboard operation
huma copied to clipboard

Support Enum as Separate Type in OpenAPI Spec for Client Generators

Open neffsvg opened this issue 1 year ago • 5 comments

Is there a way to add enums as separate types in the OpenAPI specification file, rather than using inline enum definitions. This will allow client generators (e.g., TypeScript/JavaScript) to create reusable enum types that can be used by applications like Angular, React, etc.

how "enums" are generated right now:

"ObjectFoo": {
    "properties": {
        "fooType": {
            "enum": [
                "ENUM_Value_ONE",
                "ENUM_Value_TWO",
                "ENUM_Value_THREE"
            ],
            "example": "ENUM_Value_ONE",
            "type": "string"
        }
    }
}

how "enums" could be generated as extra types:

components:
  schemas:
    FooTypeEnum:
      type: string
      enum:
        - ENUM_Value_ONE
        - ENUM_Value_TWO
        - ENUM_Value_THREE

"ObjectFoo": {
    "properties": {
        "fooType": {
            "$ref": "#/components/schemas/FooTypeEnum"
        }
    }
}

neffsvg avatar Oct 21 '24 09:10 neffsvg

@neffsvg +1

It's kinda related to this one https://github.com/danielgtaylor/huma/issues/560

superstas avatar Oct 22 '24 09:10 superstas

I'm going to see if I can implement this

yursan9 avatar Oct 24 '24 03:10 yursan9

In case that helps, here is a workaround that relies on implementing the SchemaProvider interface for enums

type InstitutionKind string

const (
	Lab                           InstitutionKind = "Lab"
	FoundingAgency      InstitutionKind = "FundingAgency"
	SequencingPlatform InstitutionKind = "SequencingPlatform"
	Other                        InstitutionKind = "Other"
)

var InstitutionKindValues = []InstitutionKind{
	Lab,
	FoundingAgency,
	SequencingPlatform,
	Other,
}

// Register enum in OpenAPI specification
func (u InstitutionKind) Schema(r huma.Registry) *huma.Schema {
  if r.Map()["InstitutionKind"] == nil {
    schemaRef := r.Schema(reflect.TypeOf(""), true, "InstitutionKind")
    schemaRef.Title = "InstitutionKind"
    for _, v := range InstitutionKindValues {
      schemaRef.Enum = append(schemaRef.Enum, string(v))
    }
    r.Map()["InstitutionKind"] = schemaRef
  }
  return &huma.Schema{Ref: "#/components/schemas/InstitutionKind"}
}

I use code generation to keep the enum values array up to date and avoid typing boilerplate for every enum type.

lsdch avatar Nov 05 '24 08:11 lsdch

Thanks @lsdch - this is super useful.

jamesleeht avatar Dec 04 '24 15:12 jamesleeht

If anyone else is using @lsdch solution above, it's likely that the InstitutionKindValues might be a map (especially if your enum is generated). If this is the case, you can write a simple function to sort the map:

import "sort"

func SortMap[T any](m map[string]T) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	return keys
}
// Register enum in OpenAPI specification
func (u InstitutionKind) Schema(r huma.Registry) *huma.Schema {
  if r.Map()["InstitutionKind"] == nil {
    schemaRef := r.Schema(reflect.TypeOf(""), true, "InstitutionKind")
    schemaRef.Title = "InstitutionKind"
    for _, v := range SortMap(InstitutionKindValues) {
      schemaRef.Enum = append(schemaRef.Enum, string(v))
    }
    r.Map()["InstitutionKind"] = schemaRef
  }
  return &huma.Schema{Ref: "#/components/schemas/InstitutionKind"}
}

This will stop the openapi.yaml diff from changing each time.

jamesleeht avatar Dec 13 '24 07:12 jamesleeht