From 19b7383b5e3edf1de044fc084b446ec129be2946 Mon Sep 17 00:00:00 2001 From: Marc Izquierdo Date: Mon, 20 Apr 2026 13:50:10 +0200 Subject: [PATCH] Allow overriding generic types with definitions in `map-types` --- docparse/docparse.go | 3 + docparse/jsonschema.go | 66 +++++++++++++++++++ .../openapi2/src/generics-map-types/in.go | 31 +++++++++ .../openapi2/src/generics-map-types/test.conf | 9 +++ .../openapi2/src/generics-map-types/want.yaml | 47 +++++++++++++ 5 files changed, 156 insertions(+) create mode 100644 testdata/openapi2/src/generics-map-types/in.go create mode 100644 testdata/openapi2/src/generics-map-types/test.conf create mode 100644 testdata/openapi2/src/generics-map-types/want.yaml diff --git a/docparse/docparse.go b/docparse/docparse.go index 0fbd43e..6f8661f 100644 --- a/docparse/docparse.go +++ b/docparse/docparse.go @@ -574,6 +574,9 @@ func MapType(prog *Program, in string) (kind, format string) { if v, ok := prog.Config.MapTypes[in]; ok { kind = v } + if kind == "" { + return + } if v, ok := prog.Config.MapFormats[in]; ok { format = v } diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index 964fcf3..b539b96 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -475,6 +475,11 @@ start: default: return nil, fmt.Errorf("unknown generic type: %T", typ.X) } + if mapped, err := genericMapTypeLookup(prog, &p, genericsPkg, genericsIdent.Name, ref, typ.Index); err != nil { + return nil, err + } else if mapped { + return &p, nil + } if err := fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Index); err != nil { return nil, fmt.Errorf("generic fieldToSchema: %v", err) } @@ -497,6 +502,11 @@ start: default: return nil, fmt.Errorf("unknown generic type: %T", typ.X) } + if mapped, err := genericMapTypeLookup(prog, &p, genericsPkg, genericsIdent.Name, ref, typ.Indices...); err != nil { + return nil, err + } else if mapped { + return &p, nil + } err = fillGenericsSchema(prog, &p, tagName, ref, genericsPkg, genericsIdent, generics, typ.Indices...) if err != nil { return nil, fmt.Errorf("generic fieldToSchema: %v", err) @@ -534,6 +544,62 @@ start: return &p, nil } +// applyMapType checks if key has a map-types entry and, if so, writes the +// mapped type and optional format into p. Returns true when a mapping was found. +func applyMapType(prog *Program, p *Schema, key string) bool { + mappedType, mappedFormat := MapType(prog, key) + if mappedType == "" { + return false + } + p.Type = JSONSchemaType(mappedType) + if mappedFormat != "" { + p.Format = mappedFormat + } + return true +} + +// genericMapTypeLookup checks if a generic type has a map-types override in the +// config. It first tries the full instantiated key (e.g. "pkg.Foo[Bar]"), then +// falls back to the main type key (e.g. "pkg.Foo"). Returns true if a mapping +// was found and the schema was populated. +func genericMapTypeLookup( + prog *Program, + p *Schema, + genericsPkg string, + genericsName string, + ref Reference, + indices ...ast.Expr, +) (bool, error) { + mainKey := genericsPkg + "." + genericsName + + var args []string + for _, idx := range indices { + arg, argPkg, err := findTypeIdent(idx, ref.Package) + if err != nil { + // Unresolvable type arguments are normal for partial or external + // packages; skip the full-key lookup and fall through to the main key. + args = nil + break + } + if argPkg != ref.Package { + args = append(args, argPkg+"."+arg.Name) + } else { + args = append(args, arg.Name) + } + } + + // Try full instantiated key first, e.g. "pkg.Foo[string, Bar]". + if len(args) > 0 { + fullKey := mainKey + "[" + strings.Join(args, ", ") + "]" + if applyMapType(prog, p, fullKey) { + return true, nil + } + } + + // Fall back to main type key, e.g. "pkg.Foo". + return applyMapType(prog, p, mainKey), nil +} + // fillGenericsSchema fills the schema with the generic type information. As the // types can be different for every generics declaration they will need to be a // anonymous object in the schema output instead of a reusable reference. diff --git a/testdata/openapi2/src/generics-map-types/in.go b/testdata/openapi2/src/generics-map-types/in.go new file mode 100644 index 0000000..aff2ce2 --- /dev/null +++ b/testdata/openapi2/src/generics-map-types/in.go @@ -0,0 +1,31 @@ +package generics_map_types + +// Wrapper wraps a value with metadata. +type Wrapper[T any] struct { + // Value is the wrapped value. + Value T + // Extra is additional metadata. + Extra string +} + +// GroupBy groups results by a field. +type GroupBy[T any] struct { + // Field is the grouping field. + Field T + // Label is the display label. + Label string +} + +type reqRef struct { + // FullKey is mapped via full instantiated key Wrapper[string]. + FullKey Wrapper[string] + // MainKey is mapped via main type key GroupBy (matches all instantiations). + MainKey GroupBy[int] + // Unmapped is not in map-types so it expands as an object. + Unmapped Wrapper[int] +} + +// POST /path +// +// Request body: reqRef +// Response 200: {empty} diff --git a/testdata/openapi2/src/generics-map-types/test.conf b/testdata/openapi2/src/generics-map-types/test.conf new file mode 100644 index 0000000..daf2a50 --- /dev/null +++ b/testdata/openapi2/src/generics-map-types/test.conf @@ -0,0 +1,9 @@ +# Map specific generic instantiation to a primitive type + format. +# Full key: "pkg.Type[ArgType]" +map-types + generics-map-types.Wrapper[string] string + generics-map-types.GroupBy string + +# Map format for the full key. +map-format + generics-map-types.Wrapper[string] date-time diff --git a/testdata/openapi2/src/generics-map-types/want.yaml b/testdata/openapi2/src/generics-map-types/want.yaml new file mode 100644 index 0000000..8db096d --- /dev/null +++ b/testdata/openapi2/src/generics-map-types/want.yaml @@ -0,0 +1,47 @@ +swagger: "2.0" +info: + title: x + version: x +consumes: + - application/json +produces: + - application/json +paths: + /path: + post: + operationId: POST_path + consumes: + - application/json + produces: + - application/json + parameters: + - name: generics-map-types.reqRef + in: body + required: true + schema: + $ref: '#/definitions/generics-map-types.reqRef' + responses: + 200: + description: 200 OK (no data) +definitions: + generics-map-types.reqRef: + title: reqRef + type: object + properties: + FullKey: + description: FullKey is mapped via full instantiated key Wrapper[string]. + type: string + format: date-time + MainKey: + description: MainKey is mapped via main type key GroupBy (matches all instantiations). + type: string + Unmapped: + description: Unmapped is not in map-types so it expands as an object. + type: object + properties: + Extra: + description: Extra is additional metadata. + type: string + Value: + description: Value is the wrapped value. + type: integer