How to Use Soft Cardinality Checks for Relationship Validation in Neomodel

Enabling soft_cardinality_check in the global Neomodel configuration allows relationship cardinality violations to log warnings instead of raising AttemptedCardinalityViolation exceptions, letting you debug or migrate data without breaking transactions.

When working with the neo4j-contrib/neomodel Object Graph Mapper (OGM), you define relationship cardinalities such as One or ZeroOrOne to enforce structural constraints on your graph. By default, violating these constraints aborts the operation and raises an exception. However, Neomodel provides a soft cardinality check mode that prints a warning while still creating the relationship, which is invaluable for data migration scripts, legacy imports, or debugging complex relationship logic.

Understanding Relationship Cardinality Enforcement

Neomodel validates cardinality constraints in the relationship manager classes located in neomodel/sync_/cardinality.py and neomodel/async_/cardinality.py. When you attempt to connect a node to a relationship that already exceeds its defined cardinality (for example, connecting a second pet to an owner when the relationship is defined as One), the library normally raises AttemptedCardinalityViolation and rolls back the transaction.

Enabling Soft Cardinality Checks

Configuration Flag Location

The soft check behavior is controlled by the soft_cardinality_check boolean flag defined in neomodel/config.py (lines 30-35). By default, this value is set to False, meaning the library enforces hard cardinality constraints.

Runtime Activation

To enable soft checks at runtime, import the global configuration object and toggle the flag before executing your relationship logic:

from neomodel import get_config

config = get_config()
config.soft_cardinality_check = True

Once enabled, this setting applies globally to both synchronous and asynchronous relationship managers.

How Soft Checks Work Under the Hood

Synchronous Implementation

In neomodel/sync_/cardinality.py, the check_cardinality() method (lines 22-28) inspects the soft_cardinality_check flag before raising an exception. When the flag is True, the method prints a warning message to stdout and returns without raising AttemptedCardinalityViolation, allowing the relationship connection to proceed.

Asynchronous Implementation

The identical logic exists in neomodel/async_/cardinality.py (lines 22-28). The async relationship managers reference the same global configuration object, ensuring consistent behavior whether you are using await node.connect() or synchronous node.connect() calls.

Warning Message Format

When a violation occurs in soft mode, Neomodel outputs a message in this format:


Cardinality violation detected : Node already has <description>, should not connect more. Soft check is enabled so the relationship will be created.

You can capture this output using io.StringIO or redirect it to a logging handler for audit trails.

Practical Implementation Examples

The following example demonstrates both hard and soft cardinality enforcement using a simple Owner and Pet model where an owner should only have one pet (One cardinality):

from neomodel import StructuredNode, StringProperty, RelationshipFrom, get_config
from neomodel.exceptions import AttemptedCardinalityViolation
import io
from unittest.mock import patch

# ----------------------------------------------------------------------

# Model definitions

# ----------------------------------------------------------------------

class Owner(StructuredNode):
    name = StringProperty()
    # One-to-one relationship: an owner should have only one pet

    pets = RelationshipFrom('Pet', 'OWNED_BY', cardinality=One)

class Pet(StructuredNode):
    name = StringProperty()


# ----------------------------------------------------------------------

# 1. Hard cardinality enforcement (default behavior)

# ----------------------------------------------------------------------

owner_a = Owner(name="Alice").save()
owner_b = Owner(name="Bob").save()
fluffy = Pet(name="Fluffy").save()

# First connection succeeds

owner_a.pets.connect(fluffy)

# Second connection attempt raises exception

try:
    owner_b.pets.connect(fluffy)  # Violates One cardinality

except AttemptedCardinalityViolation as e:
    print(f"Hard check blocked connection: {e}")


# ----------------------------------------------------------------------

# 2. Soft cardinality check - logs warning but creates relationship

# ----------------------------------------------------------------------

config = get_config()
config.soft_cardinality_check = True  # Enable soft mode

# Capture the warning output

stream = io.StringIO()
with patch("sys.stdout", new=stream):
    # This would normally raise an exception, but now succeeds

    owner_b.pets.connect(fluffy)
    
    warning_output = stream.getvalue()

print(f"Soft check warning: {warning_output.strip()}")

# Relationship now exists despite cardinality violation

For asynchronous models, the configuration works identically:

from neomodel import AsyncStructuredNode, AsyncRelationshipFrom, get_config

config = get_config()
config.soft_cardinality_check = True

# Async usage

await owner.pets.connect(pet)  # Will print warning instead of raising

When to Use Soft Cardinality Checks

Enable soft cardinality validation when you need to:

  • Migrate legacy data that contains historical violations you plan to clean up later
  • Debug complex relationship logic without breaking execution flow
  • Import bulk datasets where you want to log violations for manual review rather than aborting the entire transaction
  • Test boundary conditions in development environments

Remember that soft checks only affect the Python validation layer; they do not disable any database-level constraints you may have created separately in Neo4j.

Summary

  • Soft cardinality checks in Neomodel allow relationship violations to log warnings instead of raising AttemptedCardinalityViolation exceptions.
  • The feature is controlled by the soft_cardinality_check boolean flag in neomodel/config.py, accessed via get_config().
  • When enabled, the check_cardinality() methods in neomodel/sync_/cardinality.py and neomodel/async_/cardinality.py print a warning message and permit the relationship creation.
  • This mode is useful for data migration, debugging, and legacy imports where you need to identify violations without breaking workflow continuity.

Frequently Asked Questions

What is the default value of soft_cardinality_check in Neomodel?

By default, soft_cardinality_check is set to False in neomodel/config.py (lines 30-35). This means Neomodel enforces hard cardinality constraints and raises AttemptedCardinalityViolation immediately when a relationship limit is exceeded.

Does enabling soft cardinality checks affect database constraints?

No, the soft cardinality check is purely an application-level feature within the Neomodel OGM. It only changes how the Python code handles validation errors. If you have created unique constraints or relationship limits directly in Neo4j using Cypher, those database-level rules remain active and will still enforce hard limits regardless of this configuration flag.

Can I use soft checks with async Neomodel models?

Yes, the soft_cardinality_check configuration applies to both synchronous and asynchronous relationship managers. The async implementation in neomodel/async_/cardinality.py references the same global configuration object as the sync version in neomodel/sync_/cardinality.py, ensuring consistent behavior across both APIs.

How do I capture soft cardinality warnings for logging?

When soft checks are enabled, Neomodel prints the warning to standard output using a print statement. To capture these warnings programmatically, redirect sys.stdout to a io.StringIO buffer during the connect operation, or patch the stdout stream in your test suite. You can then parse the captured string to extract violation details for logging frameworks like Python's logging module.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →