d2lang/d2 · error

edge key must be reserved

Error message

edge key must be reserved

What it means

When a key addresses an edge's sub-key (`mk.EdgeKey`, e.g. the part after the edge in `(a -> b).style.opacity`), `_set` requires that sub-key to be a D2 reserved keyword (style, shape, icon, etc.). Arbitrary custom keys on edges are not allowed.

Source

Thrown at d2oracle/edit.go:610

			if earliestRef != nil && scope.Range.Before(earliestRef.MapKey.Range) {
				// Since the original mk was trimmed to common, we set to the edge that
				// the ref's scope is in
				// Only replace with earliestRef.Edge if it's not a glob pattern
				if !earliestRef.Edge.Src.HasGlob() && !earliestRef.Edge.Dst.HasGlob() {
					mk.Edges[0] = earliestRef.Edge
				}
				// We can't reference an edge before it's been defined
				earliestRef.Scope.InsertAfter(earliestRef.MapKey, mk)
			} else {
				appendMapKey(scope, mk)
			}
			return nil
		}
		attrs = edge.Attributes

		if mk.EdgeKey != nil {
			if _, ok := d2ast.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]; !ok {
				return errors.New("edge key must be reserved")
			}
			reserved = true

			toSkip = 1
			mk = &d2ast.Key{
				Key:   cloneKey(mk.EdgeKey),
				Value: mk.Value,
			}

			foundMap := false
			for _, ref := range refs {
				// TODO get the most nested one
				if ref.MapKey.Value.Map != nil {
					foundMap = true
					scope = ref.MapKey.Value.Map
					for _, n := range scope.Nodes {
						if n.MapKey == nil || n.MapKey.Value.Map == nil {
							continue

View on GitHub (pinned to 0d69dca6f5)

Solutions

  1. Use a reserved edge key: `style.<prop>` (stroke, opacity, etc.), `source-arrowhead.*`, or `target-arrowhead.*`.
  2. Replace flat forms like `.color` with `.style.stroke`, `.fill` with `.style.fill`.
  3. Check the key against d2ast.ReservedKeywords before calling.
  4. For node-level custom fields remember this restriction applies only to the edge portion (mk.EdgeKey).

Example fix

// before
g, err = d2oracle.Set(g, nil, "(a -> b).color", &red)
// after
g, err = d2oracle.Set(g, nil, "(a -> b).style.stroke", &red)
Defensive patterns

Strategy: validation

Validate before calling

mk, err := d2parser.ParseMapKey(key)
if err != nil { return err }
if mk.EdgeKey != nil {
    if _, ok := d2ast.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]; !ok {
        return fmt.Errorf("%q is not a reserved edge key; use style.*, source-arrowhead.*, or target-arrowhead.*", key)
    }
}

Type guard

func edgeKeyReserved(key string) bool {
    mk, err := d2parser.ParseMapKey(key)
    if err != nil || mk.EdgeKey == nil { return false }
    _, ok := d2ast.ReservedKeywords[mk.EdgeKey.Path[0].Unbox().ScalarString()]
    return ok
}

Try / catch

err := d2oracle.Set(g, nil, key, &val)
if err != nil && err.Error() == "edge key must be reserved" {
    return fmt.Errorf("rewrite %q to use a reserved edge key like .style.stroke", key)
}

Prevention

When it happens

Trigger: Calling d2oracle.Set/Create with a key whose edge trailing key is not reserved, e.g. `(a -> b).color` or `(a -> b).myField` instead of `(a -> b).style.stroke`.

Common situations: Mistaking edge attributes for plain fields (writing `color` instead of `style.stroke`); porting object-level custom field syntax to edges; typos like `styel.opacity`.

Related errors


AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31). Data as JSON: /api/errors/ee916feb85c67301. Report an issue: GitHub.