# How to Use Spatial Properties with Neo4j Point Types in Neomodel

> Learn to use spatial properties with Neo4j Point types in Neomodel. Effortlessly store, validate, and query Cartesian and WGS84 points using the PointProperty class.

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

---

**Neomodel provides the `PointProperty` class and `NeomodelPoint` wrapper in `neomodel.contrib.spatial_properties` to store, validate, and query Neo4j's native `CartesianPoint` and `WGS84Point` types.**

Neomodel, the Python OGM for Neo4j, includes a dedicated contrib module for handling spatial data through the `neomodel.contrib.spatial_properties` package. This implementation wraps Neo4j's native Point types into Python-friendly classes that integrate seamlessly with Neomodel's property system. Understanding how to use spatial properties with Neo4j Point types enables you to build location-aware applications with proper coordinate reference system (CRS) validation.

## Understanding Neomodel's Spatial Architecture

### The NeomodelPoint Wrapper

The `NeomodelPoint` class, defined in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) at lines 57-285, serves as a thin wrapper around Shapely's `Point` object while maintaining awareness of the Coordinate Reference System (CRS). This class accepts raw coordinates, sequences, or existing points during instantiation and exposes safe accessors including `x`, `y`, `z`, `longitude`, `latitude`, and `height`. The constructor automatically determines whether the point represents geometrical (Cartesian) or geographical (WGS84) data based on the provided arguments, raising `ValueError` for dimension mismatches or missing CRS information at lines 84-102 and 119-138.

### The PointProperty Class

`PointProperty`, implemented in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) at lines 524-638, functions as the bridge between Python objects and Neo4j's database layer. This property type requires a mandatory `crs` argument during definition to enforce correct dimensionality and coordinate system compliance. The class implements three critical methods: `inflate` (lines 575-592) converts incoming Neo4j `neo4j.spatial.Point` instances into `NeomodelPoint` objects by checking SRID compatibility, while `deflate` (lines 616-636) performs the reverse translation, creating native Neo4j `CartesianPoint` or `WGS84Point` objects from `NeomodelPoint` instances while validating CRS consistency.

## Defining Models with Spatial Properties

To store spatial data, define a `StructuredNode` subclass and declare a `PointProperty` with the appropriate CRS. The module supports four CRS strings: `cartesian`, `cartesian-3d`, `wgs-84`, and `wgs-84-3d`.

```python
from neomodel import StructuredNode, StringProperty, UniqueIdProperty
from neomodel.contrib.spatial_properties import PointProperty

class Place(StructuredNode):
    uid = UniqueIdProperty()
    name = StringProperty(required=True)
    # 2D geographic coordinates (latitude/longitude)

    location = PointProperty(crs="wgs-84")

```

The `crs="wgs-84"` argument forces the property to accept only 2-D geographic coordinates.

For three-dimensional Cartesian coordinates (x, y, z), use the `cartesian-3d` CRS:

```python
class Building(StructuredNode):
    name = StringProperty(required=True)
    # 3D geometric coordinates

    position = PointProperty(crs="cartesian-3d")

```

## Creating and Persisting Spatial Data

Instantiate `NeomodelPoint` using coordinate-specific arguments or raw tuples. The constructor automatically infers the CRS when using geographical keywords (`longitude`, `latitude`) or Cartesian axes (`x`, `y`, `z`).

```python
from neomodel.contrib.spatial_properties import NeomodelPoint

# Geographic point using named parameters

paris = NeomodelPoint(longitude=2.3522, latitude=48.8566)

# Save the node

Place(name="Paris", location=paris).save()

```

`NeomodelPoint` automatically sets `crs="wgs-84"` because latitude and longitude were supplied, as implemented in the constructor logic at [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) lines 77-90.

For Cartesian coordinates, use the axis accessors:

```python

# 2D Cartesian point

origin_2d = NeomodelPoint(x=0.0, y=0.0)

# 3D Cartesian point (automatically selects cartesian-3d CRS)

origin_3d = NeomodelPoint(x=10.0, y=20.0, z=5.0)

```

## Querying Spatial Data by Distance

Leverage Neo4j's native `distance` function in Cypher queries. The `PointProperty` ensures that stored values are native Neo4j points, making them compatible with spatial functions.

```python
from neomodel import db
from neomodel.contrib.spatial_properties import NeomodelPoint

# Define search center

center = NeomodelPoint(longitude=2.35, latitude=48.85)

# Query for places within 10km

query = """
MATCH (p:Place)
WHERE distance(p.location, point({longitude: $lon, latitude: $lat})) < $maxdist
RETURN p.name, p.location
"""

results, _ = db.cypher_query(
    query,
    {"lon": center.longitude, "lat": center.latitude, "maxdist": 10000}
)

for name, point in results:
    print(f"{name}: {point}")  # point is a neo4j.spatial.Point instance

```

The `distance` function works because `location` is stored as a native Neo4j point created by `PointProperty.deflate`.

## Updating and Managing Spatial Properties

Modify spatial properties by assigning new `NeomodelPoint` instances. The `PointProperty.deflate` method validates CRS compatibility before persisting to the database.

```python

# Retrieve existing node

place = Place.nodes.get(name="Paris")

# Update to 3D Cartesian coordinates

place.location = NeomodelPoint(x=10.0, y=20.0, z=5.0)
place.save()

```

The deflation process creates a Neo4j `CartesianPoint` with SRID 9157 for 3D Cartesian data, as defined in the CRS mapping at [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) lines 28-32.

### Async Support for Spatial Data

The spatial property implementation works identically in both synchronous and asynchronous contexts. Use `AsyncStructuredNode` with the same `PointProperty` definitions.

```python
from neomodel import AsyncStructuredNode, StringProperty
from neomodel.contrib.spatial_properties import PointProperty, NeomodelPoint

class AsyncPlace(AsyncStructuredNode):
    name = StringProperty(required=True)
    location = PointProperty(crs="cartesian")

async def create_place():
    pt = NeomodelPoint(x=1, y=2)
    await AsyncPlace(name="Origin", location=pt).save()

```

Both sync and async APIs share the same spatial implementation in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py).

## Summary

- **NeomodelPoint** wraps Shapely points with CRS awareness, providing safe accessors for coordinates in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) at lines 57-285.
- **PointProperty** requires a mandatory `crs` argument (`cartesian`, `cartesian-3d`, `wgs-84`, or `wgs-84-3d`) to enforce dimensionality and validate data during inflation and deflation at lines 524-638.
- The `inflate` method converts Neo4j spatial points to `NeomodelPoint` instances by checking SRID compatibility, while `deflate` creates native Neo4j `CartesianPoint` or `WGS84Point` objects for storage.
- Spatial properties integrate seamlessly with Neo4j's Cypher `distance` function and support both synchronous and asynchronous node operations.

## Frequently Asked Questions

### What CRS values are supported by PointProperty?

Neomodel supports four Coordinate Reference System strings: `cartesian` for 2D geometric coordinates, `cartesian-3d` for 3D geometric coordinates, `wgs-84` for 2D geographic coordinates (latitude/longitude), and `wgs-84-3d` for 3D geographic coordinates including height or altitude. These mappings are defined in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) at lines 44-52.

### How does NeomodelPoint handle coordinate validation?

The `NeomodelPoint` constructor validates dimensionality and CRS consistency during instantiation. It raises `ValueError` if you mix Cartesian axes (`x`, `y`, `z`) with geographic coordinates (`longitude`, `latitude`), or if the CRS string does not match the provided coordinate dimensions. This validation logic resides in [`neomodel/contrib/spatial_properties.py`](https://github.com/neo4j-contrib/neomodel/blob/main/neomodel/contrib/spatial_properties.py) at lines 84-102 and 119-138.

### Can I use spatial properties with async models?

Yes, `PointProperty` works identically with `AsyncStructuredNode` classes. The property's `inflate` and `deflate` methods are synchronous operations that prepare data for Neo4j's driver, which handles the actual async I/O. You can define spatial properties on async models using the same `crs` argument and `NeomodelPoint` values as with standard `StructuredNode` classes.

### What happens when I query spatial data using Cypher distance functions?

When you use Neo4j's `distance` function in Cypher queries against properties defined as `PointProperty`, the database receives native Neo4j point objects (either `CartesianPoint` or `WGS84Point`) created by the `deflate` method. The `inflate` method converts returned points back to `NeomodelPoint` instances when retrieving results through Neomodel's OGM methods, though raw Cypher queries via `db.cypher_query` return `neo4j.spatial.Point` instances directly.