larksuite/cli · error

interface type %s requires an explicit Shape

Error message

interface type %s requires an explicit Shape

What it means

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.

Source

Thrown at shortcuts/common/typed_compile_data.go:167

			return nil, fmt.Errorf("array item: %w", err)
		}
		shape = typedArrayShape{Items: elementShape, MinItems: schema.minItems, MaxItems: schema.maxItems}
	case reflect.Struct:
		if implementsCustomEncoding(baseType) {
			return nil, fmt.Errorf("custom JSON type %s requires an explicit Shape", baseType)
		}
		if len(schema.enum) > 0 || hasStringConstraints(schema) || hasNumberConstraints(schema) || hasItemConstraints(schema) || schema.format != "" {
			return nil, fmt.Errorf("object field has incompatible schema constraint")
		}
		object, err := compileStructShape(baseType, input, baseType.String(), active)
		if err != nil {
			return nil, err
		}
		shape = object
	case reflect.Map:
		return nil, fmt.Errorf("map type %s requires an explicit Shape", baseType)
	case reflect.Interface:
		return nil, fmt.Errorf("interface type %s requires an explicit Shape", baseType)
	default:
		return nil, fmt.Errorf("Go type %s cannot be mapped to a ValueShape", t)
	}
	if schema.nullable != nil && *schema.nullable {
		shape = typedOneOfShape{Variants: []typedValueShape{shape, typedNullShape{}}}
	}
	return shape, nil
}

// compileStructShape walks one struct into an ObjectShape. active holds the
// struct types already open on the current recursion path, so a type that
// refers back to itself is reported as a compile error. Without the guard the
// walk never terminates and the goroutine stack is exhausted -- that is a
// fatal runtime error, not a panic, so no recover boundary can contain it and
// the whole CLI dies during command registration.
//
// Membership is scoped to the path rather than the whole walk: a type is
// removed once its fields are compiled, so the same type appearing twice as a

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Replace the interface field with a concrete struct or scalar type that models the actual content.
  2. Provide an explicit Output.Data.Shape describing the field.
  3. If the payload is genuinely arbitrary JSON, promote it: make Output.Data itself `any` so anyJSONShape applies.
  4. Split Data into variant structs (or use Overrides) instead of one interface-typed field.

Example fix

// before
type Data struct {
    Payload interface{} `json:"payload"`
}

// after
type Payload struct {
    Key   string `json:"key"`
    Value string `json:"value"`
}
type Data struct {
    Payload Payload `json:"payload"`
Defensive patterns

Strategy: type-guard

Validate before calling

func hasInterfaceField(t reflect.Type) bool {
    for t.Kind() == reflect.Pointer { t = t.Elem() }
    switch t.Kind() {
    case reflect.Interface:
        return t.NumMethod() > 0 || t != t // any non-top-level interface
    case reflect.Struct:
        for i := 0; i < t.NumField(); i++ {
            if hasInterfaceField(t.Field(i).Type) { return true }
        }
    case reflect.Slice, reflect.Array:
        return hasInterfaceField(t.Elem())
    }
    return false
}

Type guard

func isInterface(t reflect.Type) bool { return t.Kind() == reflect.Interface }

Prevention

When it happens

Trigger: 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.

Common situations: Placeholder `Details interface{}` fields in DTOs; interfaces used for testability leaking into wire structs; converting loosely typed response structs into typed Data.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/b46f787f4f6967cb. Report an issue: GitHub.