# How to Use Semi-Structured Nodes with Flexible Schemas in neomodel

> Learn to use SemiStructuredNode in neomodel for flexible schemas. Store arbitrary properties alongside declared fields while maintaining type safety for core attributes. Explore this powerful feature today.

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

---

**Use neomodel's `SemiStructuredNode` or `AsyncSemiStructuredNode` base classes to store arbitrary properties alongside declared schema fields, enabling flexible schemas while maintaining type safety for core attributes.**

The `neo4j-contrib/neomodel` library provides a Pythonic way to interact with Neo4j, balancing the database's schemaless nature with structured modeling. When you need semi-structured nodes with flexible schemas, the `SemiStructuredNode` class allows you to define strict types for essential fields while accepting dynamic, undeclared properties at runtime.

## Understanding SemiStructuredNode Architecture

The `SemiStructuredNode` implementation in [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) extends `StructuredNode` to create a dual-layer property system. This architecture preserves type checking and indexing for declared fields while treating additional properties as dynamic attributes.

### Core Components

| Component | Purpose | Implementation Location |
|-----------|---------|------------------------|
| `SemiStructuredNode` | Base class enabling flexible property storage | [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) (line 5) |
| `inflate` | Deserializes nodes from Neo4j, separating declared properties from dynamic extras | [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) (line 25) |
| `deflate` | Serializes nodes to Neo4j, merging declared and dynamic properties | [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) (line 51) |
| `InflateConflict` / `DeflateConflict` | Exceptions raised when dynamic properties collide with class methods or attributes | [`neomodel/exceptions.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/exceptions.py) |

The `inflate` method first processes declared properties through the parent class, then identifies extra keys by computing `node.keys() - registered_db_property_names`. These extra properties attach directly to the instance via `setattr`. Similarly, `deflate` extracts undeclared attributes from `__dict__` and merges them into the property dictionary sent to Neo4j.

## Implementing Semi-Structured Nodes

### Basic Model Definition

Define your model by inheriting from `SemiStructuredNode` and declaring only the fields that require type enforcement or indexing:

```python
from neomodel import StringProperty, IntegerProperty
from neomodel.contrib import SemiStructuredNode

class Person(SemiStructuredNode):
    name = StringProperty(required=True)
    age = IntegerProperty()

```

This pattern appears in the test suite at [`test/sync_/test_contrib/test_semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_contrib/test_semi_structured.py) (lines 11-14), demonstrating identical syntax to `StructuredNode` but with flexible schema capabilities.

### Storing Dynamic Properties

Instantiate nodes with arbitrary keyword arguments beyond your declared schema:

```python

# Create with both declared and dynamic properties

p = Person(name="Alice", age=30, hobby="painting", height=165).save()

# Add ad-hoc attributes after creation

p.favorite_color = "blue"
p.save()

```

The `test_save_to_model_with_extras` test case (lines 31-38 in [`test_semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test_semi_structured.py)) validates this exact workflow, confirming that dynamic properties persist through save and reload operations.

### Retrieving Flexible Data

Access dynamic properties as normal Python attributes after retrieval:

```python
alice = Person.nodes.get(name="Alice")
print(alice.hobby)          # Output: painting

print(alice.favorite_color) # Output: blue

```

The test suite confirms these extra fields survive the round-trip from Neo4j through the `inflate` method and remain accessible as instance attributes.

## Handling Schema Conflicts

The semi-structured implementation includes safety mechanisms to prevent dynamic properties from overwriting critical class methods or attributes.

### InflateConflict Prevention

When loading a node from the database, `InflateConflict` raises if a stored property name matches an existing class attribute:

```python
class Foo(SemiStructuredNode):
    name = StringProperty()

# Manually create a node with a conflicting property

db.cypher_query(
    "CREATE (n:Foo $props)",
    {"props": {"name": "Bob", "hello": "world"}}
)

# This raises InflateConflict because 'hello' is a method on the class

try:
    Foo.nodes.get(name="Bob")
except InflateConflict:
    print("Conflict detected: database property collides with class method")

```

This protection appears in [`test_semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test_semi_structured.py) (lines 63-68), demonstrating how the library prevents accidental API corruption.

### DeflateConflict Protection

When saving, `DeflateConflict` prevents you from assigning values to reserved attribute names:

```python
bob = Foo(name="Bob")
bob.hello = "override"   # 'hello' is a method, not a free property

bob.save()               # Raises DeflateConflict

```

The `test_deflate_conflict` test case (lines 71-82) validates this safety check, ensuring that dynamic properties cannot shadow class methods during serialization.

## Async Support with AsyncSemiStructuredNode

For asynchronous applications, neomodel provides `AsyncSemiStructuredNode` with identical functionality:

```python
from neomodel.contrib import AsyncSemiStructuredNode

class AsyncPerson(AsyncSemiStructuredNode):
    email = StringProperty(unique_index=True)

```

The async implementation resides in [`neomodel/contrib/async_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/async_/semi_structured.py) and mirrors the sync version's `inflate` and `deflate` logic, allowing flexible schemas in async/await contexts.

## Summary

- **SemiStructuredNode** enables flexible schemas by allowing arbitrary properties alongside declared fields in `neo4j-contrib/neomodel`.
- The implementation in [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) uses `inflate` to deserialize extra properties and `deflate` to serialize them back to Neo4j.
- **InflateConflict** and **DeflateConflict** exceptions protect against dynamic properties overwriting class methods or attributes.
- Both synchronous (`SemiStructuredNode`) and asynchronous (`AsyncSemiStructuredNode`) variants support semi-structured data patterns.
- Test coverage in [`test/sync_/test_contrib/test_semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/test/sync_/test_contrib/test_semi_structured.py) demonstrates creation, retrieval, and conflict handling workflows.

## Frequently Asked Questions

### What is the difference between StructuredNode and SemiStructuredNode?

`StructuredNode` requires all properties to be explicitly declared in the class definition, enforcing strict type checking and schema validation. `SemiStructuredNode` inherits from `StructuredNode` but adds the ability to store arbitrary, undeclared properties as dynamic attributes, making it ideal for evolving schemas or heterogeneous data while maintaining type safety for core fields.

### Can I query dynamic properties using neomodel's filter methods?

Yes, dynamic properties stored on `SemiStructuredNode` instances are persisted to Neo4j as regular node properties and can be queried using Cypher. However, since these properties are not declared in the model definition, you cannot use the standard `Person.nodes.filter(hobby="painting")` syntax for dynamic fields unless you use raw Cypher queries via `db.cypher_query()` or the `filter` method with raw Cypher expressions.

### How does neomodel prevent dynamic properties from breaking class methods?

The `inflate` and `deflate` methods in [`neomodel/contrib/sync_/semi_structured.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/sync_/semi_structured.py) explicitly check for attribute collisions. During inflation (loading from Neo4j), if a stored property name matches an existing class method or attribute, `InflateConflict` is raised. During deflation (saving to Neo4j), if you attempt to assign a value to a reserved attribute name, `DeflateConflict` is raised. These exceptions protect the model's API integrity.

### Is there a performance penalty for using SemiStructuredNode?

The performance impact is minimal. The `inflate` and `deflate` methods add only a simple set difference operation (`node.keys() - registered_db_property_names`) to separate declared from dynamic properties. This overhead is negligible compared to the network round-trip to Neo4j. However, since dynamic properties cannot be indexed through the model definition, querying large datasets by dynamic fields may require full database scans unless you manually create Neo4j indexes via Cypher.