From 73a95477518e7dba6c4092df72f02954856faebe Mon Sep 17 00:00:00 2001 From: Sean O'Grady <1761115+seanogdev@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:59:08 +0100 Subject: [PATCH 1/2] Resolve map-types for selectors and slice elements fieldToSchema's SelectorExpr branch only tried the short-alias key (e.g. "view.Date") when looking up map-types, so fully-qualified config entries like "github.com/foo/bar/view.Date" never matched cross-package references. resolveArray never called MapType at all, so []view.Date / []Date slice fields always fell through to $ref even when the element type was configured as a primitive. Retry MapType with the resolved full import path in fieldToSchema's SelectorExpr branch, and add a MapType check in resolveArray for both the pkg-qualified and fully-qualified keys before the $ref fallback. --- docparse/jsonschema.go | 42 +++++++++++++++++++--- docparse/jsonschema_test.go | 70 ++++++++++++++++++++++++++++++++++++ docparse/testdata/src/a/a.go | 9 +++++ 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index bfc76eb..73c0e13 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -416,6 +416,15 @@ start: pkg = importPath } + // Retry map-types with the resolved full import path. The initial + // lookup above uses the short package alias (e.g. `view.Date`) + // because we don't yet know the full path; retrying here lets + // fully-qualified config keys (e.g. + // `github.com/foo/bar/view.Date`) match selector references too. + if applyMapType(prog, &p, importPath+"."+name.Name) { + return &p, nil + } + if resolvType, ok := ts.Type.(*ast.ArrayType); ok { isEnum := p.Type == "enum" p.Type = "array" @@ -761,6 +770,9 @@ func resolveArray( asw := typ var name *ast.Ident + // importPath, when set, is the fully-qualified import path of the element + // type; used to retry map-types lookups with the full-path key. + var importPath string arrayStart: switch typ := asw.(type) { @@ -803,6 +815,9 @@ arrayStart: // the switch. p.Items.Type = "" name = typ + // Bare ident: the element is declared in ref.Package, so the + // full-path key equals ref.Package.typ.Name. + importPath = ref.Package // "pkg.foo" case *ast.SelectorExpr: @@ -817,12 +832,13 @@ arrayStart: name = typ.Sel // handle import aliases - _, _, importPath, err := findType(ref.File, pkg, name.Name) + _, _, resolved, err := findType(ref.File, pkg, name.Name) if err != nil { return fmt.Errorf("resolveArray: findType: %v", err) } - if !strings.HasSuffix(importPath, pkg) { - pkg = importPath + importPath = resolved + if !strings.HasSuffix(resolved, pkg) { + pkg = resolved } case *ast.MapType: @@ -833,8 +849,26 @@ arrayStart: return fmt.Errorf("fieldToSchema: unknown array type: %T", typ) } - // Check if the type resolves to a Go primitive. + // Honor map-types config for slice elements. Try the pkg-qualified + // lookup first, then the fully-qualified form, so both short keys + // like `view.Date` and fully-qualified keys match. lookup := pkg + "." + name.Name + fullLookup := importPath + "." + name.Name + for _, key := range []string{lookup, fullLookup} { + if key == "." { + continue + } + if mt, mf := MapType(prog, key); mt != "" { + items := &Schema{Type: JSONSchemaType(mt)} + if mf != "" { + items.Format = mf + } + p.Items = items + return nil + } + } + + // Check if the type resolves to a Go primitive. t, err := getTypeInfo(prog, lookup, ref.File) if err != nil { return err diff --git a/docparse/jsonschema_test.go b/docparse/jsonschema_test.go index cf74ac8..7b0047a 100644 --- a/docparse/jsonschema_test.go +++ b/docparse/jsonschema_test.go @@ -75,6 +75,76 @@ func TestFieldToProperty(t *testing.T) { }) } + t.Run("mapped types", func(t *testing.T) { + cases := []struct { + name string + mapTypes map[string]string + want map[string]*Schema + }{ + { + name: "short selector key", + mapTypes: map[string]string{ + "mail.Address": "string", + "a.bar": "string", + }, + want: map[string]*Schema{ + "b": {Type: "string"}, + "bSlice": {Type: "array", Items: &Schema{Type: "string"}}, + "pkg": {Type: "string"}, + "pkgSlice": {Type: "array", Items: &Schema{Type: "string"}}, + }, + }, + { + name: "fully-qualified key", + mapTypes: map[string]string{ + "net/mail.Address": "string", + "a.bar": "string", + }, + want: map[string]*Schema{ + "b": {Type: "string"}, + "bSlice": {Type: "array", Items: &Schema{Type: "string"}}, + "pkg": {Type: "string"}, + "pkgSlice": {Type: "array", Items: &Schema{Type: "string"}}, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ts, _, _, err := findType("./testdata/src/a/a.go", "a", "mapped") + if err != nil { + t.Fatalf("could not parse file: %v", err) + } + st, ok := ts.Type.(*ast.StructType) + if !ok { + t.Fatal("not a struct?!") + } + + prog := NewProgram(false) + prog.Config.MapTypes = tc.mapTypes + + for _, f := range st.Fields.List { + name := f.Names[0].Name + out, err := fieldToSchema(prog, name, "json", Reference{ + Package: "a", + File: "./testdata/src/a/a.go", + Context: "req", + }, f, nil) + if err != nil { + t.Fatalf("%s: %v", name, err) + } + w, ok := tc.want[name] + if !ok { + t.Fatalf("no expected schema for %s", name) + } + if d := diff.Diff(w, out); d != "" { + t.Errorf("%s: %v", name, d) + } + } + }) + } + }) + t.Run("nested", func(t *testing.T) { prog := NewProgram(false) ts, _, _, err := findType("./testdata/src/a/a.go", "a", "nested") diff --git a/docparse/testdata/src/a/a.go b/docparse/testdata/src/a/a.go index 0984578..a4b0d4a 100644 --- a/docparse/testdata/src/a/a.go +++ b/docparse/testdata/src/a/a.go @@ -44,6 +44,15 @@ type nested struct { deeper refAnother } +// mapped exercises map-types resolution against both bare-ident and +// selector references, in both single-field and slice-element form. +type mapped struct { + b bar + bSlice []bar + pkg mail.Address + pkgSlice []mail.Address +} + type customStrs []customStr type customStr string From 27b00b6cb9a4603b78621254c958df63d9ffbc29 Mon Sep 17 00:00:00 2001 From: Sean O'Grady <1761115+seanogdev@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:06:19 +0100 Subject: [PATCH 2/2] Collapse redundant MapType branches in fieldToSchema The SelectorExpr case used `t == ""` / `t != ""` gating around the canonicalType check, then re-tested `t` again afterwards. Collapsing both into a single early-return via applyMapType keeps behaviour identical and brings gocyclo back under the linter threshold after adding the full-path retry. --- docparse/jsonschema.go | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/docparse/jsonschema.go b/docparse/jsonschema.go index 73c0e13..9c7d45a 100644 --- a/docparse/jsonschema.go +++ b/docparse/jsonschema.go @@ -385,24 +385,19 @@ start: pkg = pkgSel.Name name = typ.Sel - lookup := pkg + "." + name.Name - t, f := MapType(prog, lookup) - if t == "" { - // Only check for canonicalType if this isn't mapped. - canon, err := canonicalType(ref.File, pkgSel.Name, typ.Sel) - if err != nil { - return nil, fmt.Errorf("cannot get canonical type: %v", err) - } - if canon != nil { - sw = canon - goto start - } + // Try map-types with the short package alias first. + if applyMapType(prog, &p, pkg+"."+name.Name) { + return &p, nil } - p.Format = f - if t != "" { - p.Type = JSONSchemaType(t) - return &p, nil + // Only check for canonicalType if this isn't mapped. + canon, err := canonicalType(ref.File, pkgSel.Name, typ.Sel) + if err != nil { + return nil, fmt.Errorf("cannot get canonical type: %v", err) + } + if canon != nil { + sw = canon + goto start } // Deal with array. @@ -416,11 +411,9 @@ start: pkg = importPath } - // Retry map-types with the resolved full import path. The initial - // lookup above uses the short package alias (e.g. `view.Date`) - // because we don't yet know the full path; retrying here lets + // Retry map-types with the resolved full import path so // fully-qualified config keys (e.g. - // `github.com/foo/bar/view.Date`) match selector references too. + // `github.com/foo/bar/view.Date`) also match selector references. if applyMapType(prog, &p, importPath+"."+name.Name) { return &p, nil }