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

  1. Add .Optional() to one side of the edge (usually the many/inverse side) so at least one direction is optional
  2. If the FK column should allow NULL, also keep .Field("owner_id") nullable in the column definition
  3. Decide which direction is semantically optional and mark only that one required
  4. 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

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


AI-assisted analysis of ent/ent@69d5d4deb1 (2026-09-03). Data as JSON: /api/errors/5a56bcafc95d75a0. Report an issue: GitHub.