Use when reading WebID profiles, storage roots, LDP containers, WAC or ACP resources, or converting access-control documents through the external @solid/object typed wrapper package.
Resources
1Install
npx skillscat add jeswr/solid-sdk/solid-object Install via the SkillsCat registry.
Quick reference
Read Solid RDF with @solid/object
@solid/object is currently an external dependency, not a package directory in this workspace. Use its existing typed wrappers before creating a local wrapper; move this skill beside the package if it is imported.
Common wrappers
WebIdDatasetandAgentfor profiles, names, photos, issuer values, contacts, and storage roots.ContainerDataset,Container, andResourcefor LDP listings.AclResource,Authorization, andGroupfor WAC.AccessControlResource,Policy, andMatcherfor ACP.wacToAcpandacpToWacfor explicit conversion.
import { fetchRdf } from "@jeswr/fetch-rdf";
import { WebIdDataset } from "@solid/object";
import { DataFactory } from "n3";
const { dataset } = await fetchRdf(webId, { fetch });
const agent = new WebIdDataset(dataset, DataFactory).mainSubject;
const storages = [...(agent?.storageUrls ?? [])];Rules for untrusted RDF
- A WebID may advertise multiple storage roots or issuers. Require an explicit selection where ambiguity matters.
- Typed getters can throw on a foreign term of the wrong kind or datatype. Guard each fallback predicate independently so one malformed preferred value does not hide a valid fallback.
- For interoperable dates, read the lexical string and parse leniently when foreign producers may not use the exact expected datatype.
- Multi-valued typed accessors can be all-or-nothing. For hostile RDF, iterate matching objects and filter valid term types individually.
- Use one
DataFactoryconsistently. - These wrappers are primarily readers. Implement writes in explicit
TermWrappersubclasses, serialize the complete dataset, and persist conditionally. - Never hand-parse
.aclor.acrdocuments when the typed models cover the operation.
Agent persona
When this work is delegated to a sub-agent, spawn the solid-data-modeler persona from
`.claude/agents/solid-data-modeler.md` — it routes through this skill.
Orchestration: .claude/agents/solid-app-orchestration.md.
@solid/object — typed wrappers for Solid data
Pre-built @rdfjs/wrapper classes for the Solid data you read most (companion guide:
`AGENTS.md`). Check here before writing your own wrapper — profile and
container reading already exist.
npm install @solid/object n3 # n3 is required at runtime but NOT a dependency — install itPure ESM, Node ≥ 24. @rdfjs/wrapper comes transitively. Root import re-exports everything;
subpaths ./webid, ./solid, ./acp also exist.
What ships (v0.6.0)
| Class | Entry pattern | Key members |
|---|---|---|
WebIdDataset (DatasetWrapper) |
new WebIdDataset(dataset, DataFactory).mainSubject |
mainSubject: Agent | undefined — the subject carrying solid:oidcIssuer |
Agent (TermWrapper) |
new Agent(webIdIri, dataset, DataFactory) |
name (vcard:fn → foaf:name → IRI tail), vcardFn, foafName, email, phone, website, photoUrl, organization, role, title, pimStorage / solidStorage / storageUrls: Set<string>, oidcIssuer: Set<string>, knows: Set<string>, hasEmail, hasTelephone |
ContainerDataset |
new ContainerDataset(dataset, DataFactory).container |
container: Container | undefined |
Container / Resource |
via ContainerDataset |
contains: Set<Resource>; per resource: id, name, isContainer, title, label, modified, size, type, mimeType |
Email / Telephone (+ datasets) |
the only classes with setters (emailAddress = …) |
|
WAC: AclResource, Authorization, Group |
typed .acl access |
|
ACP: AccessControlResource, Policy, Matcher |
typed .acr access |
|
| Converters | wacToAcp(source: AclResource, target: DatasetCore) / acpToWac(source, target) |
translate between the two languages; throw WacToAcpError / AcpToWacError (extend TranslationError) |
Reading a profile
import { fetchRdf } from "@jeswr/fetch-rdf";
import { WebIdDataset } from "@solid/object";
import { DataFactory } from "n3";
const { dataset } = await fetchRdf(webId);
const me = new WebIdDataset(dataset, DataFactory).mainSubject;
console.log(me?.name, [...(me?.storageUrls ?? [])]);Multiple storages: storageUrls is a Set because WebIDs may advertise severalpim:storage values. When the app needs one, present the list and let the user choose —
never silently take the first.
Rendering a profile — ProfileAgent reference class
Agent.name covers vcard:fn → foaf:name only. For UI rendering you want the full fallback
chains (name, avatar, bio, nickname, homepage). That class does not exist upstream yet, so this
skill bundles a compile-verified reference implementation —
`profile-agent.ts` — extending Agent. Copy it into src/lib/ and adapt;
it is a candidate for upstreaming into @solid/object, at which point prefer the upstream
version. Pattern: each getter tries predicates in a documented preference order and survives
wrong term types (a literal where an IRI is expected skips to the next candidate, never throws).
Writing
All the classes above (except Email/Telephone) are read-only getters. The write path is
always your own TermWrapper subclass with …As setters over the same dataset, then serialise
- conditional
PUT—AGENTS.md§Writing data. Do not look for asave()here.
Gotchas
| Gotcha | Detail |
|---|---|
mainSubject is undefined when the profile carries no solid:oidcIssuer |
Surface a clear error: the WebID is not usable for Solid login |
| Fresh CSS pod profiles are bare | Only foaf:Person + solid:oidcIssuer — name is undefined and storageUrls is empty until the profile is seeded (AGENTS.md §Servers) |
organization / role / title expect NamedNode objects |
A literal value in the data throws TermTypeError — wrap reads of untrusted profiles (see first() in profile-agent.ts) |
LiteralAs.date is STRICT — it throws on a foreign serialiser's date |
LiteralAs.date calls ensureDatatype(term, xsd:date, xsd:dateTime), so it throws LiteralDatatypeError on a date-only string typed xsd:string, an untyped/plain literal (N3 types these xsd:string), or any value another app serialised loosely. When parsing RDF a different app wrote (interop), read the lexical value with LiteralAs.string (no datatype check) and parse leniently yourself — const d = new Date(value); return Number.isNaN(d.getTime()) ? undefined : d; — never LiteralAs.date, or one malformed/foreign literal throws and breaks the whole document's parse. (Verified against @rdfjs/wrapper 0.34.0 LiteralAs; learned reading SolidOS cal:dtstart — a date-only "2016-06-23" literal — in the Pod Manager /schedule interop, jeswr/solid-pod-manager src/lib/schedule.ts.) |
Multi-valued reads (SetFrom/rdf:type) are all-or-nothing |
SetFrom.subjectPredicate(this, RDF_TYPE, NamedNodeAs.string, …) throws if any one object fails its mapping — so a subject typed a <ValidType>, "garbage-literal" loses its entire type set, and a valid note/profile then fails its types.has(...) check and is silently dropped. For a multi-valued predicate from untrusted RDF, read it per object off the dataset and filter, never via the throwing set accessor: const types = new Set<string>(); for (const q of dataset.match(s, namedNode(RDF_TYPE), null)) if (q.object.termType === "NamedNode") types.add(q.object.value); (Verified @rdfjs/wrapper 0.34.0; learned reconciling foreign rdf:type triples in jeswr/solid-chat-interop.) |
| Fallback-chain reads mask each other — guard per predicate | A typed getter that prefers one predicate and falls back to another — LiteralAs.date(DCT_CREATED) ?? LiteralAs.date(AS_PUBLISHED) — skips the valid fallback when the preferred literal is malformed, because the strict read throws before ?? is evaluated (caught higher up, the fallback is never tried → the value is wrongly dropped). Wrap each predicate read individually so a bad preferred value falls through: tryRead(() => …pref) ?? tryRead(() => …fallback) where tryRead = (f) => { try { return f(); } catch { return undefined; } }. (Learned in jeswr/solid-chat-interop reconciling AS2.0 ↔ SolidOS LongChat.) |
LiteralAs.string is lenient (the safe read for untrusted literals) |
Unlike LiteralAs.date, LiteralAs.string does not datatype-check — it returns the lexical value of whatever literal is present (e.g. an xsd:integer 42 reads as "42"), so it never throws on a foreign serialiser's loose typing. This is why the interop pattern reads with LiteralAs.string and parses leniently. (NamedNodeAs.string is the strict one for that kind — a literal where an IRI is expected throws TermTypeError; guard it like the date case.) (Verified @rdfjs/wrapper 0.34.0 in jeswr/solid-chat-interop.) |
n3 must be installed explicitly |
It is a devDependency upstream, not a runtime dependency |
One DataFactory everywhere |
Mixing factories breaks term equality |
| Access control | Never hand-parse .acl/.acr — these typed classes + the converters are the only sanctioned path (AGENTS.md §Access control) |