Skip to content

feat: Introduce Value and Record mapping to custom object types #1633

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2025

Conversation

injectives
Copy link
Contributor

@injectives injectives commented Apr 3, 2025

Please note that this is a feature preview.

This update brings support for mapping org.neo4j.driver.Value and org.neo4j.driver.Record to custom object types.

org.neo4j.driver.Value

The following new method has been introduced to org.neo4j.driver.Value:

<T> T as(Class<T> targetClass)

It maps the value to the given type providing that is it supported.

Basic Mapping

Supported destination types depend on the value org.neo4j.driver.types.Type, please see the supported mappings below.

  • TypeSystem#BOOLEAN -> boolean, Boolean
  • TypeSystem#BYTES -> byte[]
  • TypeSystem#STRING -> String
  • TypeSystem#INTEGER -> long, Long, int, Integer, double, Double, float, Float
  • TypeSystem#FLOAT -> long, Long, int, Integer, double, Double, float, Float
  • TypeSystem#PATH -> Path
  • TypeSystem#POINT -> Point
  • TypeSystem#DATE -> LocalDate
  • TypeSystem#TIME -> OffsetTime
  • TypeSystem#LOCAL_TIME -> LocalTime
  • TypeSystem#LOCAL_DATE_TIME -> LocalDateTime
  • TypeSystem#DATE_TIME -> ZonedDateTime, OffsetDateTime
  • TypeSystem#DURATION -> IsoDuration, java.time.Period (only when seconds = 0 and nanoseconds = 0 and no overflow happens), java.time.Duration (only when months = 0 and days = 0 and no overflow happens)
  • TypeSystem#NULL -> null
  • TypeSystem#LIST -> List
  • TypeSystem#MAP -> Map
  • TypeSystem#NODE -> Node
  • TypeSystem#RELATIONSHIP -> Relationship

Object Mapping

Mapping of user-defined properties to user-defined types is supported for the following value types:

  • TypeSystem#NODE
  • TypeSystem#RELATIONSHIP
  • TypeSystem#MAP

Example (using the Neo4j Movies Database):

// assuming the following Java record
public record Movie(String title, String tagline, long released) {}
// the nodes may be mapped to Movie instances
var movies = driver.executableQuery("MATCH (movie:Movie) RETURN movie")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("movie").as(Movie.class))
        .toList();
}

Note that Object Mapping is an alternative to accessing the user-defined values in a org.neo4j.driver.types.MapAccessor. If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like Spring Data Neo4j.

The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the constructor parameters must either have org.neo4j.driver.mapping.Property annotation or have a matching name that is available at runtime (note that the constructor parameter names are typically changed by the compiler unless either the compiler -parameters option is used or they belong to the cannonical constructor of java.lang.Record). The name matching is case-sensitive.

Additionally, the org.neo4j.driver.mapping.Property annotation may be used when mapping a property with a different name to java.lang.Record cannonical constructor parameter.

The constructor selection criteria is the following (top priority first):

  • Maximum matching properties.
  • Minimum mismatching properties.

The constructor search is done in the order defined by the java.lang.Class#getDeclaredConstructors and is finished either when a full match is found with no mismatches or once all constructors have been visited.

At least 1 property match must be present for mapping to work.

A null value is used for arguments that don't have a matching property. If the argument does not accept null value (this includes primitive types), an alternative constructor that excludes it must be available.

Example with optional property (using the Neo4j Movies Database):

// assuming the following Java record
public record Person(String name, long born) {
    // alternative constructor for values that don't have 'born' property available
    public Person(@Property("name") String name) {
        this(name, -1);
    }
}
// the nodes may be mapped to Person instances
var persons = driver.executableQuery("MATCH (person:Person) RETURN person")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("person").as(Person.class))
        .toList();

Types with generic parameters defined at the class level are not supported. However, constructor arguments with specific types are permitted.

Example (using the Neo4j Movies Database):

// assuming the following Java record
public record Acted(List<String> roles) {}
// the relationships may be mapped to Acted instances
var actedList = driver.executableQuery("MATCH ()-[acted:ACTED_IN]-() RETURN acted")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("acted").as(Acted.class))
        .toList();

On the contrary, the following record would not be mapped because the type information is insufficient:

public record Acted<T>(List<T> roles) {}

Wildcard type value is not supported.

org.neo4j.driver.Record

The following new method has been introduced to org.neo4j.driver.Record:

<T> T as(Class<T> targetClass)

It maps values of this record to properties of the given type providing that is it supported.

Example (using the Neo4j Movies Database):

// assuming the following Java record
public record MovieInfo(String title, String director, List<String> actors) {}
// the record values may be mapped to MovieInfo instances
var movies = driver.executableQuery("MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person) RETURN movie.title as title, director.name AS director, collect(actor.name) Ators")
        .execute()
        .records()
        .stream()
        .map(record -> record.as(MovieInfo.class))
        .toList();
}

It follows the same set of rules and has the same requirements as described in the Object Mapping Value section above.

@injectives injectives marked this pull request as draft April 3, 2025 11:13
@injectives injectives force-pushed the feature/value-mapper-updated branch 3 times, most recently from b75a146 to bc857bd Compare April 4, 2025 22:02
@injectives injectives force-pushed the feature/value-mapper-updated branch 2 times, most recently from 6510b5c to cf2ee02 Compare April 11, 2025 15:16
@injectives injectives changed the title feat: Introduce Mapping To Type Support feat: Introduce Value and Record mapping to custom object types Apr 11, 2025
@injectives injectives force-pushed the feature/value-mapper-updated branch 2 times, most recently from 043ee17 to d7cd56e Compare April 14, 2025 09:35
@injectives injectives marked this pull request as ready for review April 14, 2025 09:37
@injectives injectives force-pushed the feature/value-mapper-updated branch 5 times, most recently from 0060dcc to 5fc5a37 Compare April 14, 2025 10:09
Please note that this is a [feature preview](https://github.com/neo4j/neo4j-java-driver/blob/5.0/README.md#preview-features).

This update brings support for mapping `org.neo4j.driver.Value` and `org.neo4j.driver.Record` to custom object types.

 ## org.neo4j.driver.Value

The following new method has been introduced to `org.neo4j.driver.Value`:
```java
<T> T as(Class<T> targetClass)
```

It maps the value to the given type providing that is it supported.

 ### Basic Mapping

Supported destination types depend on the value `org.neo4j.driver.types.Type`, please see the supported mappings below.

- `TypeSystem#BOOLEAN` -> `boolean`, `Boolean`
- `TypeSystem#BYTES` -> `byte[]`
- `TypeSystem#STRING` -> `String`
- `TypeSystem#INTEGER` -> `long`, `Long`, `int`, `Integer`, `double`, `Double`, `float`, `Float`
- `TypeSystem#FLOAT` -> `long`, `Long`, `int`, `Integer`, `double`, `Double`, `float`, `Float`
- `TypeSystem#PATH` -> `Path`
- `TypeSystem#POINT` -> `Point`
- `TypeSystem#DATE` -> `LocalDate`
- `TypeSystem#TIME` -> `OffsetTime`
- `TypeSystem#LOCAL_TIME` -> `LocalTime`
- `TypeSystem#LOCAL_DATE_TIME` -> `LocalDateTime`
- `TypeSystem#DATE_TIME` -> `ZonedDateTime`, `OffsetDateTime`
- `TypeSystem#DURATION` -> `IsoDuration`, `java.time.Period` (only when `seconds = 0` and `nanoseconds = 0` and no overflow happens), `java.time.Duration` (only when `months = 0` and `days = 0` and no overflow happens)
- `TypeSystem#NULL` -> `null`
- `TypeSystem#LIST` -> `List`
- `TypeSystem#MAP` -> `Map`
- `TypeSystem#NODE` -> `Node`
- `TypeSystem#RELATIONSHIP` -> `Relationship`

 ### Object Mapping

Mapping of user-defined properties to user-defined types is supported for the following value types:
- `TypeSystem#NODE`
- `TypeSystem#RELATIONSHIP`
- `TypeSystem#MAP`

Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)):
```java
// assuming the following Java record
public record Movie(String title, String tagline, long released) {}
// the nodes may be mapped to Movie instances
var movies = driver.executableQuery("MATCH (movie:Movie) RETURN movie")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("movie").as(Movie.class))
        .toList();
}
```
Note that Object Mapping is an alternative to accessing the user-defined values in a `org.neo4j.driver.types.MapAccessor`. If Object Graph Mapping (OGM) is needed, please use a higher level solution built on top of the driver, like [Spring Data Neo4j](https://neo4j.com/docs/getting-started/languages-guides/java/spring-data-neo4j/).

The mapping is done by matching user-defined property names to target type constructor parameters. Therefore, the constructor parameters must either have `org.neo4j.driver.mapping.Property` annotation or have a matching name that is available at runtime (note that the constructor parameter names are typically changed by the compiler unless either the compiler `-parameters` option is used or they belong to the cannonical constructor of `java.lang.Record`). The name matching is case-sensitive.

Additionally, the `org.neo4j.driver.mapping.Property` annotation may be used when mapping a property with a different name to `java.lang.Record` cannonical constructor parameter.

The constructor selection criteria is the following (top priority first):
- Maximum matching properties.
- Minimum mismatching properties.

The constructor search is done in the order defined by the `java.lang.Class#getDeclaredConstructors` and is finished either when a full match is found with no mismatches or once all constructors have been visited.

At least 1 property match must be present for mapping to work.

A `null` value is used for arguments that don't have a matching property. If the argument does not accept `null` value (this includes primitive types), an alternative constructor that excludes it must be available.

Example with optional property (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)):
```java
// assuming the following Java record
public record Person(String name, long born) {
    // alternative constructor for values that don't have 'born' property available
    public Person(@Property("name") String name) {
        this(name, -1);
    }
}
// the nodes may be mapped to Person instances
var persons = driver.executableQuery("MATCH (person:Person) RETURN person")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("person").as(Person.class))
        .toList();
```
Types with generic parameters defined at the class level are not supported. However, constructor arguments with specific types are permitted.

Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)):
```java
// assuming the following Java record
public record Acted(List<String> roles) {}
// the relationships may be mapped to Acted instances
var actedList = driver.executableQuery("MATCH ()-[acted:ACTED_IN]-() RETURN acted")
        .execute()
        .records()
        .stream()
        .map(record -> record.get("acted").as(Acted.class))
        .toList();
```
On the contrary, the following record would not be mapped because the type information is insufficient:
```java
public record Acted<T>(List<T> roles) {}
```
Wildcard type value is not supported.

 ## org.neo4j.driver.Record

The following new method has been introduced to `org.neo4j.driver.Record`:
```java
<T> T as(Class<T> targetClass)
```

It maps values of this record to properties of the given type providing that is it supported.

Example (using the [Neo4j Movies Database](https://github.com/neo4j-graph-examples/movies)):
```java
// assuming the following Java record
public record MovieInfo(String title, String director, List<String> actors) {}
// the record values may be mapped to MovieInfo instances
var movies = driver.executableQuery("MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie)<-[:DIRECTED]-(director:Person) RETURN movie.title as title, director.name AS director, collect(actor.name) Ators")
        .execute()
        .records()
        .stream()
        .map(record -> record.as(MovieInfo.class))
        .toList();
}
```

It follows the same set of rules and has the same requirements as described in the `Object Mapping` `Value` section above.
@injectives injectives force-pushed the feature/value-mapper-updated branch from 5fc5a37 to 32d49b0 Compare April 14, 2025 10:19
Copy link
Contributor

@michael-simons michael-simons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonderful commit message, very well explained.
I quickly had a look at ObjectMapper and friends, nothing to complain. If you want me to look at something in detail, please point me to it.

I like that it's actually not much change.

@injectives injectives merged commit 61e99b3 into neo4j:5.0 Apr 14, 2025
19 checks passed
@injectives injectives deleted the feature/value-mapper-updated branch April 16, 2025 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants