# How to Create Relationships with Different Cardinality Types in Neomodel

> Master Neomodel relationships with our guide on cardinality types ZeroOrOne, One, ZeroOrMore, and OneOrMore. Learn to define precise connection limits for your Neo4j graph.

- Repository: [Neo4j Contrib/neomodel](https://github.com/neo4j-contrib/neomodel)
- Tags: how-to-guide
- Published: 2026-03-08

---

**Use the `cardinality` parameter in `RelationshipTo` or `RelationshipFrom` and pass one of four manager classes—`ZeroOrMore`, `ZeroOrOne`, `One`, or `OneOrMore`—to enforce exactly how many target nodes a relationship may connect.**

Neomodel, the Python Object Graph Mapper for Neo4j, provides precise control over relationship constraints through cardinality managers. When you create relationships with different cardinality types in neomodel, you define exactly how many nodes can participate in a relationship, preventing invalid graph states at runtime according to the source code in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py).

## Understanding Relationship Cardinality Managers

The cardinality system is implemented in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py). Each manager subclasses `RelationshipManager` and overrides connection and disconnection logic to enforce constraints:

- **ZeroOrMore**: The default manager allowing any number of relationships (0 to infinity), defined in [`neomodel/sync_/relationship_manager.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/relationship_manager.py) (lines 616-622).
- **ZeroOrOne**: Permits zero or exactly one target node (optional singular relationship), implemented in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py) (lines 14-30).
- **One**: Requires exactly one target node at all times (mandatory singular relationship), implemented in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py) (lines 106-155).
- **OneOrMore**: Requires at least one target node (mandatory collection), implemented in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py) (lines 63-89).

## Defining Cardinality in Model Definitions

To apply cardinality constraints, pass the desired manager class to the `cardinality` parameter when declaring `RelationshipTo`, `RelationshipFrom`, or `Relationship` (for bidirectional links). The constructors for these relationship definitions reside in [`neomodel/sync_/relationship_manager.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/relationship_manager.py) (lines 24-30).

```python
from neomodel import StructuredNode, RelationshipTo, ZeroOrOne, One, OneOrMore

class Employee(StructuredNode):
    # Optional single relationship

    manager = RelationshipTo("Manager", "REPORTS_TO", cardinality=ZeroOrOne)
    
    # Mandatory single relationship

    company = RelationshipTo("Company", "WORKS_FOR", cardinality=One)
    
    # Mandatory collection (at least one)

    projects = RelationshipTo("Project", "WORKS_ON", cardinality=OneOrMore)

```

## Cardinality Types in Practice

### ZeroOrMore (Default Behavior)

When you omit the `cardinality` parameter, neomodel uses `ZeroOrMore`. This imposes no limits on relationship count.

```python
from neomodel import StructuredNode, RelationshipTo

class Monkey(StructuredNode):
    dryers = RelationshipTo("HairDryer", "OWNS_DRYER")  # ZeroOrMore by default

# Usage

m = Monkey(name="tim").save()
h1 = HairDryer(version=1).save()
h2 = HairDryer(version=2).save()

m.dryers.connect(h1)
m.dryers.connect(h2)  # Succeeds: no cardinality limit

assert len(m.dryers.all()) == 2

```

*Source: [`test/sync_/test_cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_cardinality.py) (lines 83-94)*

### ZeroOrOne (Optional Singular Relationship)

The `ZeroOrOne` manager enforces at most one target node. Attempting to connect a second target raises `AttemptedCardinalityViolation`.

```python
from neomodel import StructuredNode, RelationshipTo, ZeroOrOne
from neomodel.exceptions import AttemptedCardinalityViolation

class Monkey(StructuredNode):
    driver = RelationshipTo("ScrewDriver", "HAS_SCREWDRIVER", cardinality=ZeroOrOne)

m = Monkey(name="bob").save()
s1 = ScrewDriver(version=1).save()
s2 = ScrewDriver(version=2).save()

m.driver.connect(s1)  # First connection succeeds

# Attempting second connection raises exception

try:
    m.driver.connect(s2)
except AttemptedCardinalityViolation:
    print("Cannot connect more than one screwdriver")
    

# To replace, use reconnect

m.driver.reconnect(s1, s2)  # s2 replaces s1

assert m.driver.single().version == 2

```

*Source: [`test/sync_/test_cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_cardinality.py) (lines 110-128)*

### One (Mandatory Singular Relationship)

The `One` manager requires exactly one target node at all times. It raises `AttemptedCardinalityViolation` when disconnecting the sole relationship and `CardinalityViolation` when accessing a missing relationship.

```python
from neomodel import StructuredNode, RelationshipTo, One
from neomodel.exceptions import AttemptedCardinalityViolation, CardinalityViolation

class Monkey(StructuredNode):
    toothbrush = RelationshipTo("ToothBrush", "HAS_TOOTHBRUSH", cardinality=One)

m = Monkey(name="jerry").save()
tb = ToothBrush(name="bristle").save()

# Must create the relationship

m.toothbrush.connect(tb)

# Cannot disconnect the only relationship

try:
    m.toothbrush.disconnect(tb)
except AttemptedCardinalityViolation:
    print("Cannot remove the mandatory toothbrush")

# Accessing without relationship raises CardinalityViolation

fresh_monkey = Monkey(name="new").save()
try:
    fresh_monkey.toothbrush.all()
except CardinalityViolation:
    print("Mandatory relationship missing")

```

*Source: [`test/sync_/test_cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_cardinality.py) (lines 152-166)*

### OneOrMore (Mandatory Collection)

The `OneOrMore` manager enforces at least one target node. You cannot disconnect the final relationship, and accessing an empty collection raises `CardinalityViolation`.

```python
from neomodel import StructuredNode, RelationshipTo, OneOrMore
from neomodel.exceptions import AttemptedCardinalityViolation, CardinalityViolation

class Monkey(StructuredNode):
    car = RelationshipTo("Car", "HAS_CAR", cardinality=OneOrMore)

m = Monkey(name="jerry").save()
c1 = Car(version=2).save()
c2 = Car(version=3).save()

# Empty collection raises CardinalityViolation

try:
    m.car.all()
except CardinalityViolation:
    print("Must have at least one car")

# Add first car (now valid)

m.car.connect(c1)

# Can add more

m.car.connect(c2)
assert len(m.car.all()) == 2

# Can disconnect one

m.car.disconnect(c2)
assert len(m.car.all()) == 1

# Cannot disconnect the last one

try:
    m.car.disconnect(c1)
except AttemptedCardinalityViolation:
    print("Cannot remove the last car")

```

*Source: [`test/sync_/test_cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_cardinality.py) (lines 151-188)*

## Handling Cardinality Violations

When cardinality constraints are violated, neomodel raises specific exceptions defined in [`neomodel/exceptions.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/exceptions.py) (lines 12-28):

- **`AttemptedCardinalityViolation`**: Raised when an operation would create an invalid state, such as adding a second target to a `ZeroOrOne` relationship or removing the last target from a `OneOrMore` relationship.
- **`CardinalityViolation`**: Raised when the current graph state violates the constraint, such as accessing a `One` relationship that has no target nodes.

Both exceptions inherit from `NeomodelException`, allowing you to catch all neomodel-specific errors with a single exception handler.

## Summary

- Use the `cardinality` parameter in `RelationshipTo`, `RelationshipFrom`, or `Relationship` to enforce relationship constraints in neomodel.
- **ZeroOrMore** is the default manager allowing unlimited relationships, implemented in [`neomodel/sync_/relationship_manager.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/relationship_manager.py) (lines 616-622).
- **ZeroOrOne** enforces at most one target node, raising `AttemptedCardinalityViolation` on duplicate connections (lines 14-30).
- **One** requires exactly one target node, forbidding disconnection and raising `CardinalityViolation` when missing (lines 106-155).
- **OneOrMore** requires at least one target node, preventing removal of the final relationship (lines 63-89).
- All cardinality managers reside in [`neomodel/sync_/cardinality.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/cardinality.py) and raise specific exceptions defined in [`neomodel/exceptions.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/exceptions.py).

## Frequently Asked Questions

### What is the default cardinality if I don't specify one?

If you omit the `cardinality` parameter, neomodel uses **ZeroOrMore**, which imposes no limits on the number of target nodes. This manager is defined in [`neomodel/sync_/relationship_manager.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/sync_/relationship_manager.py) (lines 616-622) and allows you to connect any number of nodes without raising cardinality violations.

### Can I change the cardinality of an existing relationship?

No, cardinality is defined at the model level and enforced by Python manager classes. Changing cardinality requires updating your model definition and migrating your data to comply with the new constraints. For example, changing from `ZeroOrMore` to `One` requires ensuring every source node has exactly one target node before the code enforces the new rule.

### What happens if I violate a cardinality constraint?

Neomodel raises specific exceptions based on the violation type. **AttemptedCardinalityViolation** occurs when you try to create an invalid state, such as connecting a second node to a `ZeroOrOne` relationship or disconnecting the last node from a `OneOrMore` relationship. **CardinalityViolation** occurs when accessing a relationship that violates its constraint, such as calling `all()` on a `One` relationship with no targets. Both exceptions are defined in [`neomodel/exceptions.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/exceptions.py).

### How do I replace a node in a ZeroOrOne or One relationship?

Use the `reconnect(old_node, new_node)` method provided by the cardinality managers. This atomically disconnects the existing node and connects the new one, maintaining the cardinality constraint throughout the operation. For example, `employee.manager.reconnect(old_manager, new_manager)` replaces the manager while ensuring the relationship never violates the `ZeroOrOne` or `One` constraint.