package main import ( "reflect" "strings" "time" ) // The REST API's responses are Go structs (apitypes.go); their JSON Schemas // in the OpenAPI document are derived from those types, so what is documented // is what is sent. Struct tags: `json` as usual (omitempty = optional), `doc` // is the description, `enum` a comma-separated list of the allowed strings. // Named structs become components/schemas. type schemaBuilder struct { defs map[string]any } func newSchemaBuilder() *schemaBuilder { return &schemaBuilder{defs: map[string]any{}} } var timeType = reflect.TypeOf(time.Time{}) // of returns the schema of t (a $ref for a named struct). func (b *schemaBuilder) of(t reflect.Type) map[string]any { switch t.Kind() { case reflect.Pointer: return nullable(b.of(t.Elem())) case reflect.Bool: return map[string]any{"type": "boolean"} case reflect.String: return map[string]any{"type": "string"} case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return map[string]any{"type": "integer"} case reflect.Float32, reflect.Float64: return map[string]any{"type": "number"} case reflect.Slice, reflect.Array: return map[string]any{"type": "array", "items": b.of(t.Elem())} case reflect.Map: return map[string]any{"type": "object", "additionalProperties": b.of(t.Elem())} case reflect.Struct: if t == timeType { return map[string]any{"type": "string", "format": "date-time"} } if t.Name() == "" { return b.object(t) } if _, ok := b.defs[t.Name()]; !ok { b.defs[t.Name()] = nil // against recursion b.defs[t.Name()] = b.object(t) } return map[string]any{"$ref": "#/components/schemas/" + t.Name()} } return map[string]any{} // interfaces: any value } func (b *schemaBuilder) object(t reflect.Type) map[string]any { props := map[string]any{} var required []string for i := range t.NumField() { f := t.Field(i) name, opts, _ := strings.Cut(f.Tag.Get("json"), ",") if !f.IsExported() || name == "-" { continue } if name == "" { name = f.Name } s := b.of(f.Type) if d := f.Tag.Get("doc"); d != "" { s["description"] = d } if e := f.Tag.Get("enum"); e != "" { s["enum"] = strings.Split(e, ",") } props[name] = s if !strings.Contains(","+opts+",", ",omitempty,") { required = append(required, name) } } out := map[string]any{"type": "object", "properties": props} if len(required) > 0 { out["required"] = required } return out } // nullable lets a schema also be null. func nullable(s map[string]any) map[string]any { if typ, ok := s["type"].(string); ok { out := map[string]any{} for k, v := range s { out[k] = v } out["type"] = []string{typ, "null"} return out } return map[string]any{"anyOf": []any{s, map[string]any{"type": "null"}}} }