larksuite/cli · error

map type %s requires an explicit Shape

Error message

map type %s requires an explicit Shape

What it means

Map types cannot be given a statically derived schema: their keys and values are not fixed at compile time, so the shape compiler cannot produce an ObjectShape or a constrained value shape. Fields typed as any map are rejected and require an explicit Shape or a concrete struct replacement.

Source

Thrown at shortcuts/common/typed_compile_data.go:165

		elementShape, err := shapeForType(baseType.Elem(), elementSchema, input, active)
		if err != nil {
			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.
//

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Define a concrete struct with fixed, json-tagged fields instead of the map.
  2. Provide an explicit Output.Data.Shape that describes the map's value shape.
  3. If the content is truly dynamic, make the whole Data type `any` (anyJSONShape) and validate at runtime.
  4. Encode known key sets as struct fields and keep only genuinely open content out of typed Data.

Example fix

// before
type Data struct {
    Labels map[string]string `json:"labels"`
}

// after
type Data struct {
    Labels struct {
        Env  string `json:"env"`
        Team string `json:"team"`
    } `json:"labels"`
Defensive patterns

Strategy: validation

Validate before calling

func hasMapField(t reflect.Type) bool {
    for t.Kind() == reflect.Pointer { t = t.Elem() }
    switch t.Kind() {
    case reflect.Map:
        return true
    case reflect.Struct:
        for i := 0; i < t.NumField(); i++ {
            if hasMapField(t.Field(i).Type) { return true }
        }
    case reflect.Slice, reflect.Array:
        return hasMapField(t.Elem())
    }
    return false
}

Type guard

func isMap(t reflect.Type) bool { return t.Kind() == reflect.Map }

Prevention

When it happens

Trigger: A Data struct field typed map[string]X, map[string]string, or similar is encountered by shapeForType's reflect.Map case during compileData/collectArgFields.

Common situations: Modeling dynamic key/value bags (labels, metadata, properties); porting from generic JSON code where maps were convenient; OpenAPI additionalProperties-style fields.

Related errors


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