The Neo4j Type System

At this point, we should take a look at the Cypher type system. Despite Neo4j being written in Java (the j in Neo4j stands for Java after all), there are some discrepancies between the types available in Cypher and native Java types.

Some values like strings, numbers, booleans, dates, and nulls map directly to Java types but more complex types like nodes, relationship, points, durations need special handling.

Java Types to Neo4j/Cypher Types
Java Type Neo4j Cypher Type Notes

null,

null

List

List,Array

Neo4j can only store a flat array containing strings, booleans or numbers.

Map

Map

Boolean

Boolean

Long

Integer

Double

Float

String

String

byte[]

byte[]

LocalDate

LocalDate

See Temporal Types

Time

Time

See Temporal Types

LocalTime

LocalTime

See Temporal Types

DateTime

DateTime

See Temporal Types

LocalDateTime

LocalDateTime

See Temporal Types

IsoDuration

Duration

Point

Point

Node

Node

See Nodes & Relationships

Relationship

Relationship

See Nodes & Relationships

Path

Path

See Nodes & Relationships

You can use the as{Type}() method on any Value type or nested structure to cast underlying values to expected types. See the API docs for Value for more information.

For example, in the code block below, the year value is cast as a Number.

java
Value nodeValue = row.get("movie");

// key names
Iterable<String> keys = nodeValue.keys();
// number of values / length of list
nodeValue.size();
// get all contained values
Iterable<Value> values = nodeValue.values();

// treat value as type, e.g. Node, Relationship, Path or primitive
Node node = nodeValue.asNode();

// string-key accessors
Value titleValue = node.get("title");
String title = titleValue.asString();
Number year = node.get("year").asNumber();
Integer releaseYear = node.get("year").asInt(0);

// index based accessors for lists and records
nodeValue.get(0);

Let’s take a look at some of these types in more detail.

Numbers

For numeric values the main confusion comes from the naming, while Neo4j itself can store all kinds of Java primitive values for a unified surface, Cypher only exposes one floating point type called Float (equivalent to 64-bit double) and one integer type called Integer (equivalent to 64-bit long).

Cypher itself has functions like toFloat() and toInteger() respectively and driver parameters of other types are automatically coerced.

The driver’s Value type can return most Java numeric types via as{Type}() methods. Only BigDecimal/BigInteger for arbitrary precision math are not supported.

Temporal Types

The Temporal types used in the Cypher type system mirror the java.time.* types, so there are few surprises. The only difference is IsoDuration for which the driver provides a custom type.

Learn how to interact with Neo4j from Java using the Neo4j Java DriverTemporal Types
Type Description Example Access Function

Date

Represents an instant capturing the date, but not the time, nor the timezone.

2020-01-02

asDate

DateTime

Represents an instant capturing the date, the time and the timezone identifier.

2020-01-02T01:02:03+04:00

asDateTime

LocalDateTime

Represents an instant capturing the date and the time, but not the timezone.

2020-01-02T01:02:03

asLocalDateTime

LocalTime

Represents an instant capturing the time of day, but not the date, nor the timezone.

12:34:56

asLocalTime

OffsetTime

Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date.

12:34:56+04:00

asOffsetTime

IsoDuration

Represents a duration between two dates or timestamps (different resolutions). Has individual accessors for the parts.

P1M12D

asIsoDuration

java
Working with Temporal types
// Driver consumes regular java.time.* datatypes
// Temporal Types from Value
var released = nodeValue.get("released");
released.asLocalDate();
released.asLocalDateTime();
released.asLocalTime();
released.asOffsetDateTime();

// custom duration type
IsoDuration duration = released.asIsoDuration();

Nodes & Relationships

Nodes and Relationships are both returned as similar types with a common superclass Entity.

As an example, let’s take the following code snippet:

java
Return Nodes and Relationships
// Execute a query within a read transaction
Result res = session.readTransaction(tx -> tx.run("""
                MATCH path = (person:Person)-[actedIn:ACTED_IN]->(movie:Movie)
                RETURN path, person, actedIn, movie,
                       size ( (person)-[:ACTED]->() ) as movieCount,
                       exists { (person)-[:DIRECTED]->() } as isDirector
                LIMIT 1
        """));

Nodes

We can retrieve the person value using the .get() method on the row and then turn it into a Node via asNode().

java
// Get a node
Node person = row.get("person").asNode();

The value assigned to the person variable will be the instance of a Node. Node is a type provided by the Neo4j Java Driver to represent the information held in Neo4j for a node.

An instance of a Node has three parts:

java
Working with Node Objects
var nodeId = person.id(); // (1)
var labels = person.labels(); // (2)
var properties = person.asMap(); // (3)
  1. id - representing the internal ID for the node.

  2. labels - an Iterable of String values, eg. ['Person', 'Actor']

  3. properties - A Java Map containing all the properties for the node.
    eg. {"name": "Tom Hanks", "tmdbId": "31" }

Properties can also be retrieved from Entity instances with the get(name) method which then returns a Value that has to be converted further.

Internal IDs

Internal IDs should be treated as opaque values and just sent back to the database as you get them. These ids can be re-used, a best practice is to always look up a node by its business key and label rather than relying on an internal ID.

Relationships

Relationship objects are also Entity instances, they also include an id, a type and properties.

java
Working with Relationships
var actedIn = row.get("actedIn").asRelationship();

var relId = actedIn.id(); // (1)
String type = actedIn.type(); // (2)
var relProperties = actedIn.asMap(); // (3)
var startId = actedIn.startNodeId(); // (4)
var endId = actedIn.endNodeId(); // (5)
  1. identity - the internal ID for the relationship.

  2. type - the type of the relationship, eg - ACTED_IN

  3. properties - A Java Map containing all the properties for the relationship.
    eg. {"role": "Woody" }

  4. startNodeId - representing the internal ID for the node at the start of the relationship

  5. endNodeId - representing the internal ID for the node at the end of the relationship

Paths

If you return a path of nodes and relationships, they will be returned as an instance of a Path.

java
Working with Path Objects
Path path = row.get("path").asPath();

Node start = path.start(); // (1)
Node end = path.end(); // (2)
var length = path.length(); // (3)
Iterable<Path.Segment> segments = path; // (4)
Iterable<Node> nodes = path.nodes(); // (5)
Iterable<Relationship> rels = path.relationships(); // (6)
  1. start - the node starting the path

  2. end - the node ending the path

  3. length - A count of the number of segments within the path

  4. segments - A path is an Iterable of Path.Segment

  5. nodes - An Iterable of Nodes of the Path

  6. relationships - An Iterable of Relationships of the Path

Path Segments

A path is split into segments representing each relationship in the path. For example, say we have a path of (p:Person)-[:ACTED_IN]->(m:Movie)-[:IN_GENRE]->(g:Genre), there would be two segments.

  1. (p:Person)-[:ACTED_IN]->(m:Movie)

  2. (m:Movie)-[:IN_GENRE]->(g:Genre)

java
Iterating over Segments
path.forEach(segment -> {
    System.out.println(segment.start());
    System.out.println(segment.end());
    System.out.println(segment.relationship());
});

The PathSegment object has three properties:

  • relationship - A Relationship object representing that part of the path.

  • start - start node for this path segment *

  • end - end node for this path segment *

* Start and End nodes within the Path Segment object

The start and end nodes on the PathSegment may differ from the start and end nodes of the relationship itself if the relationship was traversed in reverse direction.

Converting these values en masse

There may be times when you need to convert many Neo4j types back into native Java types. For example, when retrieving a set of properties.

For this the Value and Record type has three functions

  • asObject() recursively converts a Value into the appropriate Java objects

  • asMap() converts a Value into a Java Map, it can take a callback function to customize conversion of individual keys and values

  • list() converts a Value into a Java List, it can take a callback function to customize conversion of individual values

The function are recursive, and will handle nested objects and arrays.

Additional helper functions

Value has additional helper functions

  • isTrue and isFalse for boolean values

  • isNull for null checks

  • isEmpty for lists and maps

Check Your Understanding

1. Accessing Node Properties

Which property would you access to retrieve the "name" property for each person?

Select the correct option in the code block below.

java
var res = session.executeRead(tx ->
    tx.run("""
        MATCH (p:Person)-[:ACTED_IN]->(:Movie {title: $title})
        RETURN p
        LIMIT 10
    """,
    Values.parameters("title", "Toy Story"))
)

var names = res.stream().map(row -> {
    return row.get('p')./*select:properties.name*/
})
  • ❏ name

  • ❏ property['name']

  • ✓ get("name")

  • ❏ properties[0]

Hint

There is a dedicated method for retrieving properties.

Solution

Properties can be accessed on nodes and relationships using the .get() method - for example node.get("name").

2. Temporal Accessors

Which of the following functions does the Neo4j Value not support for temporal types?

  • asOffsetTime()

  • asIsoDuration()

  • asTimestamp()

  • asLocalDateTime()

Hint

Neo4j supports five temporal types.

Solution

The only unsupported method above is asTimestamp()

Lesson Summary

In this lesson you have learned how to handle some of the more complex objects returned by a Cypher statement. As we progress through this module, you will use the knowledge gained so far to read data from, and write data back to the database.

In the next Challenge, you will modify code to read from the database.