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 {
continueView on GitHub (pinned to 0d69dca6f5)
Solutions
- Use a reserved edge key: `style.<prop>` (stroke, opacity, etc.), `source-arrowhead.*`, or `target-arrowhead.*`.
- Replace flat forms like `.color` with `.style.stroke`, `.fill` with `.style.fill`.
- Check the key against d2ast.ReservedKeywords before calling.
- 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
- Map intuitive names to reserved ones (color -> style.stroke, transparency -> style.opacity)
- Check d2ast.ReservedKeywords when generating keys programmatically
- Restrict edge attribute pickers in UIs to reserved keyword lists
- Spell-check reserved segments (style, shape, icon, label) before calling
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
- can only set one edge at a time
- can only delete one edge at a time
- edgeKey must be an edge
- edgeKey must refer to an existing edge
- edge not found
AI-assisted analysis of d2lang/d2@0d69dca6f5 (2026-08-31).
Data as JSON: /api/errors/ee916feb85c67301.
Report an issue: GitHub.