Skip to content

JSON-LD parser silently drops list-valued entries under "@container": "@type" #3538

Open

Description

@satra

Summary

When a context term declares "@container": "@type" and the type key's value is a list of node objects, rdflib produces no triples at all and raises no error. The same document parses correctly with pyld. A single node object (not a list) under the same term works fine, which localises the defect to list handling.

This is silent data loss: the parse succeeds, the graph is empty, and nothing indicates a problem.

Minimal reproducer

"""rdflib silently drops list-valued entries under an "@container": "@type" term.

"Expected" is what pyld, the JSON-LD reference implementation, produces for the same document.
"""
import json
import rdflib
from pyld import jsonld

CTX = {
    "@version": 1.1,
    "ex": "http://example.org/",
    "byType": {"@container": "@type", "@id": "@graph"},
    "Person": "ex:Person",
    "name": "ex:name",
}
ALICE = {"@id": "ex:alice", "name": "Alice"}
BOB = {"@id": "ex:bob", "name": "Bob"}

CASES = [
    ("type key holds a LIST  (two people)", {"@context": CTX, "byType": {"Person": [ALICE, BOB]}}),
    ("type key holds a DICT  (one person)", {"@context": CTX, "byType": {"Person": ALICE}}),
]

print(f"rdflib {rdflib.__version__}")
for label, doc in CASES:
    native = rdflib.Graph()
    native.parse(data=json.dumps(doc), format="json-ld")
    viapyld = rdflib.Graph()
    viapyld.parse(data=json.dumps(jsonld.expand(doc)), format="json-ld")
    verdict = "ok" if len(native) == len(viapyld) else "SILENT DATA LOSS"
    print(f"  {label}:  rdflib {len(native)} triples, expected {len(viapyld)}   {verdict}")

Actual output

rdflib 7.6.0
  type key holds a LIST  (two people):  rdflib 0 triples, expected 4   SILENT DATA LOSS
  type key holds a DICT  (one person):  rdflib 2 triples, expected 2   ok

Expected

Both rows should agree with pyld. JSON-LD 1.1 §9.8 (Type Maps) permits a type map's value to be a node object or an array of node objects.

Versions affected

Identical result — 0 triples, expected 4 — on every released version tested:

rdflib list-valued type key
6.3.2 0 triples (expected 4)
7.0.0 0 triples (expected 4)
7.1.4 0 triples (expected 4)
7.6.0 (latest) 0 triples (expected 4)

Environment: CPython 3.12, macOS arm64, pyld 3.3.0 as the reference. Installed via uv run --with rdflib==<version>.

Where it appears to come from

rdflib/plugins/parsers/jsonld.py:489, the TYPE-container branch of _parse_container:

elif v11 and TYPE in term.container:
    return [
        (
            self._add_type(
                context,
                (
                    {ID: context.expand(o) if term.type == VOCAB else o}
                    if isinstance(o, str)
                    else o
                ),
                k,
            )
            if isinstance(o, (dict, str)) and k not in context.get_keys(NONE)
            else o
        )
        for k, o in obj.items()
    ]

The comprehension handles o being a dict or str. When o is a list, the guard isinstance(o, (dict, str)) is false and the list is returned unchanged as a single element, so a list of node objects is then handed downstream as though it were one node object and is dropped.

Note the dict case only works when the term also has "@id": "@graph"; a @container: @type term with no @id is rejected by pyld as invalid JSON-LD, so the reproducer above uses the valid form.

Why this matters in practice

The BIDS provenance specification under review (bids-standard/bids-specification#2099) declares exactly this shape:

"Records": {"@container": "@type", "@id": "@graph"}

and every record group in it (Files, prov:Entity, Activities, Software) is a JSON array, including singletons. A conforming BIDS-Prov document therefore loses everything when read with rdflib. On a real 6.1 MB provenance graph, pyld expands to 78,681 triples while rdflib 7.6.0 returns 2 — the two metadata keys that happen to sit outside the container. No warning is emitted.

Workaround

Expanding with pyld first and handing the expanded document to rdflib recovers the full graph — 78,681 triples on the same file, matching pyld's own count:

g = rdflib.Graph()
g.parse(data=json.dumps(jsonld.expand(doc)), format="json-ld")

Possibly related

I could not find an existing issue for this specific defect. Happy to open a PR if the fix in _parse_container is as narrow as it looks.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions