FileNode/query with hasParentId: false returns nodes with non-null parentId

Issue Description

I am currently testing the FileNode implementation and encountered a minor issue when trying to fetch the list of files and folders at the root level (i.e., those without a parentId).

According to the specification section FileNode/query (draft-ietf-jmap-filenode-03), I passed hasParentId: false in the filter. I expected the returned files/folders to have parentId: null. However, the response still contains records where parentId is not null.

Am I missing something or misconfiguring the query?

Expected Behavior

Only root-level FileNodes (where parentId is null) should be returned.

Actual Behavior

The result includes records with a valid, non-null parentId.

Reproduction Steps

Call FileNode/query with the filter {“hasParentId”: false}.

Observe the returned records in the response.

Relevant Log Output

{
“using”: [
“urn:ietf:params:jmap:core”,
“urn:ietf:params:jmap:filenode”
],
“methodCalls”: [
[
“FileNode/query”,
{
“accountId”: “b”,
“filter”: {
“hasParentId”: false
}
},
“fnq0”
],
[
“FileNode/get”,
{
“accountId”: “b”,
“#ids”: {
“resultOf”: “fnq0”,
“name”: “FileNode/query”,
“path”: “/ids”
},
“properties”: [
“id”,
“parentId”,
“name”,
“type”,
“blobId”,
“size”,
“created”,
“updated”,
“myRights”,
“shareWith”
]
},
“fng0”
]
]
}

Stalwart Version

v0.16.x

Installation Method

Binary (Linux)

Database Backend

RocksDB

Blob Storage

S3-compatible

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The source code tells the full story. Looking at the FileNodeQuery implementation:

The filter hasParentId: false in the JMAP spec maps to FileNodeFilter::IsTopLevel in the server code, and that logic is correctly implemented:

FileNodeFilter::IsTopLevel(is_top_level) => {
    filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
        cache.resources.iter().filter_map(|r| {
            if is_top_level == r.parent_id().is_none() {
                Some(r.document_id)
            } else {
                None
            }
        }),
    )));
}

However, lower in the same match block, there is a catch-all arm that silently discards FileNodeFilter::ParentId(_) (and several others) as a no-op, and more critically, the hasParentId filter (IsTopLevel) is only handled in the first match arm. The real question is whether the parser is correctly mapping the JSON key hasParentId to FileNodeFilter::IsTopLevel versus FileNodeFilter::ParentId.

This is a confirmed bug in the server source code. Here’s what’s happening:

The JMAP spec uses hasParentId: false as the filter property name, but Stalwart’s FileNode/query implementation has mapped that concept to an internal filter called isTopLevel. The deserializer only recognizes "isTopLevel" as the JSON key – there is no handler for "hasParentId" in the filter deserialization code (crates/jmap-proto/src/object/file_node.rs, line 522). As a result, passing {"hasParentId": false} in the filter is silently ignored (falls through to the _T catch-all), so no filtering is applied and all nodes are returned.

Workaround: Until this is fixed, use {"isTopLevel": true} as your filter instead of {"hasParentId": false}:

"filter": {
    "isTopLevel": true
}

This maps to the correct internal FileNodeFilter::IsTopLevel(true) branch, which correctly filters to only resources where parent_id is None. This is a server-side issue where the spec property name (hasParentId) is not wired up to the internal implementation (isTopLevel).

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Am I missing something or misconfiguring the query?

Yes, the latest spec is draft-ietf-jmap-filenode-14.