{"record":{"id":"b46f787f4f6967cb","repo":"larksuite/cli","slug":"interface-type-s-requires-an-explicit-shape","errorCode":null,"errorMessage":"interface type %s requires an explicit Shape","messagePattern":"interface type (.+?) requires an explicit Shape","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shortcuts/common/typed_compile_data.go","lineNumber":167,"sourceCode":"\t\t\treturn nil, fmt.Errorf(\"array item: %w\", err)\n\t\t}\n\t\tshape = typedArrayShape{Items: elementShape, MinItems: schema.minItems, MaxItems: schema.maxItems}\n\tcase reflect.Struct:\n\t\tif implementsCustomEncoding(baseType) {\n\t\t\treturn nil, fmt.Errorf(\"custom JSON type %s requires an explicit Shape\", baseType)\n\t\t}\n\t\tif len(schema.enum) > 0 || hasStringConstraints(schema) || hasNumberConstraints(schema) || hasItemConstraints(schema) || schema.format != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"object field has incompatible schema constraint\")\n\t\t}\n\t\tobject, err := compileStructShape(baseType, input, baseType.String(), active)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tshape = object\n\tcase reflect.Map:\n\t\treturn nil, fmt.Errorf(\"map type %s requires an explicit Shape\", baseType)\n\tcase reflect.Interface:\n\t\treturn nil, fmt.Errorf(\"interface type %s requires an explicit Shape\", baseType)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"Go type %s cannot be mapped to a ValueShape\", t)\n\t}\n\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","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/shortcuts/common/typed_compile_data.go#L149-L185","documentation":"Interface-typed fields (other than the top-level `any` Data escape hatch) have no static schema, so the compiler cannot infer a ValueShape and rejects them. Only Output.Data itself may be an empty interface; nested interface fields must be replaced by concrete types or an explicit Shape.","triggerScenarios":"A nested Data struct field typed interface{}, io.Reader-like interfaces, or any non-empty interface is hit by shapeForType's reflect.Interface case during registration.","commonSituations":"Placeholder `Details interface{}` fields in DTOs; interfaces used for testability leaking into wire structs; converting loosely typed response structs into typed Data.","solutions":["Replace the interface field with a concrete struct or scalar type that models the actual content.","Provide an explicit Output.Data.Shape describing the field.","If the payload is genuinely arbitrary JSON, promote it: make Output.Data itself `any` so anyJSONShape applies.","Split Data into variant structs (or use Overrides) instead of one interface-typed field."],"exampleFix":"// before\ntype Data struct {\n    Payload interface{} `json:\"payload\"`\n}\n\n// after\ntype Payload struct {\n    Key   string `json:\"key\"`\n    Value string `json:\"value\"`\n}\ntype Data struct {\n    Payload Payload `json:\"payload\"`","handlingStrategy":"type-guard","validationCode":"func hasInterfaceField(t reflect.Type) bool {\n    for t.Kind() == reflect.Pointer { t = t.Elem() }\n    switch t.Kind() {\n    case reflect.Interface:\n        return t.NumMethod() > 0 || t != t // any non-top-level interface\n    case reflect.Struct:\n        for i := 0; i < t.NumField(); i++ {\n            if hasInterfaceField(t.Field(i).Type) { return true }\n        }\n    case reflect.Slice, reflect.Array:\n        return hasInterfaceField(t.Elem())\n    }\n    return false\n}","typeGuard":"func isInterface(t reflect.Type) bool { return t.Kind() == reflect.Interface }","tryCatchPattern":null,"preventionTips":["Use concrete structs instead of interface{} for nested Data fields.","Only Output.Data itself may be `any`; everything nested must be concrete.","Keep test doubles (mock interfaces) out of wire structs."],"tags":["go","interface","schema","typed-data"],"backgroundTag":"explicit-shape-required","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"}