{"record":{"id":"40bc7e86f7c86d7d","repo":"larksuite/cli","slug":"recursive-type-s-requires-an-explicit-shape","errorCode":null,"errorMessage":"recursive type %s requires an explicit Shape","messagePattern":"recursive type (.+?) requires an explicit Shape","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shortcuts/common/typed_compile_data.go","lineNumber":189,"sourceCode":"\tif schema.nullable != nil && *schema.nullable {\n\t\tshape = typedOneOfShape{Variants: []typedValueShape{shape, typedNullShape{}}}\n\t}\n\treturn shape, nil\n}\n\n// compileStructShape walks one struct into an ObjectShape. active holds the\n// struct types already open on the current recursion path, so a type that\n// refers back to itself is reported as a compile error. Without the guard the\n// walk never terminates and the goroutine stack is exhausted -- that is a\n// fatal runtime error, not a panic, so no recover boundary can contain it and\n// the whole CLI dies during command registration.\n//\n// Membership is scoped to the path rather than the whole walk: a type is\n// removed once its fields are compiled, so the same type appearing twice as a\n// sibling stays legal.\nfunc compileStructShape(t reflect.Type, input bool, path string, active map[reflect.Type]struct{}) (typedObjectShape, error) {\n\tif _, cyclic := active[t]; cyclic {\n\t\treturn typedObjectShape{}, fmt.Errorf(\"recursive type %s requires an explicit Shape\", t)\n\t}\n\tactive[t] = struct{}{}\n\tdefer delete(active, t)\n\n\tshape := typedObjectShape{}\n\tseen := make(map[string]string)\n\tfor i := 0; i < t.NumField(); i++ {\n\t\tfield := t.Field(i)\n\t\tif !field.IsExported() {\n\t\t\tcontinue\n\t\t}\n\t\trawJSON, ok := field.Tag.Lookup(\"json\")\n\t\tif !ok {\n\t\t\treturn typedObjectShape{}, fmt.Errorf(\"%s field %s must declare json tag\", path, field.Name)\n\t\t}\n\t\tparts := strings.Split(rawJSON, \",\")\n\t\tname := parts[0]\n\t\tif name == \"-\" {","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/shortcuts/common/typed_compile_data.go#L171-L207","documentation":"compileStructShape tracks struct types open on the current recursion path; if a type refers back to itself (directly or through fields) the walk would never terminate and exhaust the stack — a fatal, unrecoverable runtime error during command registration. It is rejected up front as a compile-time diagnostic requiring an explicit Shape for the recursive type.","triggerScenarios":"Declaring self-referential types like `type Node struct { Children []Node }` or mutual recursion A -> B -> A anywhere inside a typed Output.Data struct tree; compileStructShape detects the cycle via the `active` map.","commonSituations":"Tree/graph-shaped domain models (org charts, comment threads, linked structures) reused as Data; merged DTO packages introducing a back-reference; a field type changed to a parent type creating an accidental cycle.","solutions":["Break the recursion: replace the back-reference with a concrete leaf type or a flattened representation (e.g. parent IDs instead of child pointers).","Provide an explicit Output.Data.Shape with a bounded depth for the recursive structure.","Re-generate or bound the data model so no type on a single path appears twice (siblings repeating a type are still allowed).","If a tree is required, serialize it yourself (e.g. precomputed JSON string field modeled as string)."],"exampleFix":"// before\ntype Node struct {\n    Name     string `json:\"name\"`\n    Children []Node `json:\"children\"` // recursive\n}\n\n// after\ntype Node struct {\n    Name      string   `json:\"name\"`\n    ChildIDs  []string `json:\"child_ids\"` // flattened, non-recursive\n}","handlingStrategy":"validation","validationCode":"func isRecursive(t reflect.Type, path map[reflect.Type]struct{}) bool {\n    for t.Kind() == reflect.Pointer { t = t.Elem() }\n    if t.Kind() != reflect.Struct { return false }\n    if _, ok := path[t]; ok { return true }\n    path[t] = struct{}\n    defer delete(path, t)\n    for i := 0; i < t.NumField(); i++ {\n        if isRecursive(t.Field(i).Type, path) { return true }\n    }\n    return false\n}\n// call with map[reflect.Type]struct{}{} before registering Data","typeGuard":"func typeOnPath(t reflect.Type, active map[reflect.Type]struct{}) bool {\n    _, ok := active[t]\n    return ok\n}","tryCatchPattern":null,"preventionTips":["Never let a Data struct reference itself directly or transitively; flatten trees to parent/child IDs.","Pre-check new types with a path-scoped recursion detector before adding them to Data.","When reusing domain models in Data, audit them for back-references introduced by later refactors."],"tags":["go","recursion","schema","typed-data"],"backgroundTag":"recursive-type-rejected","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}