ent/ent · error
edges cannot be required in both directions: %s.%s <-> %s.%s
Error message
edges cannot be required in both directions: %s.%s <-> %s.%s
What it means
ent requires that at least one side of an edge pair is optional. When resolving edges, if both the inverse edge and its referenced association edge are declared required (not Optional/Ref without .Optional()), codegen fails because a mandatory relation in both directions cannot be satisfied when inserting rows. This is a schema-validation error raised by Graph.resolve.
Source
Thrown at entc/gen/graph.go:424
// - A have an edge (E) to B (not unique), and B have a back-reference unique edge (E') for E.
//
// M2O (The "Many" side, holds the reference to the "One" side).
// - A have a unique edge (E) to B, and B doesn't have a back-reference edge for E.
// - A have a unique edge (E) to B, and B have a back-reference non-unique edge (E') for E.
//
// M2M
// - A have an edge (E) to B (not unique), and B have a back-reference non-unique edge (E') for E.
// - A have an edge (E) to A (not unique).
func (g *Graph) resolve(t *Type) error {
for _, e := range t.Edges {
switch {
case e.IsInverse():
ref, ok := e.Type.HasAssoc(e.Inverse)
if !ok {
return fmt.Errorf("edge %q is missing for inverse edge: %s.%s(%s)", e.Inverse, t.Name, e.Name, e.Type.Name)
}
if !e.Optional && !ref.Optional {
return fmt.Errorf("edges cannot be required in both directions: %s.%s <-> %s.%s", t.Name, e.Name, e.Type.Name, ref.Name)
}
if ref.Type != t {
return fmt.Errorf("mismatch type for back-ref %q of %s.%s <-> %s.%s", e.Inverse, t.Name, e.Name, e.Type.Name, ref.Name)
}
e.Ref, ref.Ref = ref, e
table := t.Table()
// Name the foreign-key column in a format that wouldn't change even if an inverse
// edge is dropped (or added). The format is: "<Edge-Owner>_<Edge-Name>".
column := fmt.Sprintf("%s_%s", e.Type.Label(), snake(ref.Name))
switch a, b := ref.Unique, e.Unique; {
// If the relation column is in the inverse side/table. The rule is simple, if assoc is O2M,
// then inverse is M2O and the relation is in its table.
case a && b:
e.Rel.Type, ref.Rel.Type = O2O, O2O
case !a && b:
e.Rel.Type, ref.Rel.Type = M2O, O2M
// If the relation column is in the assoc side.View on GitHub (pinned to 69d5d4deb1)
Solutions
- Add .Optional() to one side of the edge (usually the many/inverse side) so at least one direction is optional
- If the FK column should allow NULL, also keep .Field("owner_id") nullable in the column definition
- Decide which direction is semantically optional and mark only that one required
- Regenerate code after the change
Example fix
// before (both required)
edge.From("owner", "User").Field("owner_id").Ref("pets")
// after (one side optional)
edge.From("owner", "User").Field("owner_id").Ref("pets").Optional() Defensive patterns
Strategy: validation
Validate before calling
// audit schema: at least one side of each edge pair must be Optional
func checkEdgePair(a, b Edge) error {
if !a.Optional && !b.Optional {
return fmt.Errorf("edges %s <-> %s both required", a.Name, b.Name)
}
return nil
} Try / catch
if err := entc.Generate(...); err != nil {
if strings.Contains(err.Error(), "cannot be required in both directions") {
log.Fatalf("add .Optional() to one side of the edge: %v", err)
}
log.Fatalf("ent codegen failed: %v", err)
} Prevention
- Decide requiredness direction up front: typically the M2O side is required, the O2M side is optional
- Never mark both sides of an O2O/O2M pair required; add .Optional() to the back-reference
- Remember M2M edges are always optional; the rule only bites FK-backed edges
When it happens
Trigger: Both schemas declare the edge as required, e.g. edge.From("owner", "User").Field("owner_id").Ref("pets") (no .Optional()) while User has edge.To("pets", Pet.Type) also without .Optional(), for a non-M2M relation backed by a NOT NULL foreign key.
Common situations: Copy-pasting edge definitions and forgetting .Optional() on one side; converting an optional relation to required on both ends while tightening constraints; M2M edges are exempt (always optional), so this hits O2M/M2M-by-FK and O2O pairs.
Related errors
- edge %q is missing for inverse edge: %s.%s(%s)
- mismatch type for back-ref %q of %s.%s <-> %s.%s
- edge %s.%s Through(%q, %s.Type) is allowed only on M2M edges
- edge %s.%s defined with Through(%q, %s.Type), but type %[4]s
- edge %s.%s defined with Through(%q, %s.Type), but edge canno
AI-assisted analysis of ent/ent@69d5d4deb1 (2026-09-03).
Data as JSON: /api/errors/5a56bcafc95d75a0.
Report an issue: GitHub.