Skip to content

Add withRawProperty() and withRawProperties() #1355

Description

@dahlia

Background

#1272 adds a read path for properties that Fedify's vocabulary classes have no typed accessor for: getRawPropertyValues() and getRawPropertyValue() return a property's values in expanded JSON-LD form, looked up by its full IRI. Unknown properties are retained at parse time; known properties are re-encoded from the typed fields. The issue explicitly left the other direction to #207: applications still cannot add an extension property to an object they construct, or replace one on an object they received, and have it appear in outgoing JSON-LD.

Without that, an application that publishes an extension has to subclass the vocabulary class and implement toJsonLd() itself, or post-process the serialized JSON. The second option loses the property as soon as the object is embedded in another object and serialized again.

Proposal

Vocabulary objects are immutable apart from hydration side effects, so the write side mirrors clone() rather than adding setters. Each module generated by @fedify/vocab-tools exports two functions next to the read functions from #1272:

export function withRawProperty<T extends VocabularyObject>(
  object: T,
  propertyIri: string | URL,
  values: readonly RawPropertyValue[] | null | undefined,
): Promise<T>;

export function withRawProperties<T extends VocabularyObject>(
  object: T,
  entries: Readonly<
    Record<string, readonly RawPropertyValue[] | null | undefined>
  >,
): Promise<T>;
export function withRawProperties<T extends VocabularyObject>(
  object: T,
  entries: Iterable<
    readonly [string | URL, readonly RawPropertyValue[] | null | undefined]
  >,
): Promise<T>;

RawPropertyValue stands for whatever name #1272 settles on for the expanded JSON-LD value type returned by getRawPropertyValues(), so values read from one object can be written to another unchanged. The singular and plural names count properties here, whereas the read functions count values; the documentation should say so in one sentence.

The functions return a new object of the same concrete class and never modify the argument. They are asynchronous because a known IRI is decoded into its typed field, and some decoders are asynchronous (nested fromJsonLd(), key import).

Semantics

  • An empty array, null, and undefined all remove the property. In the record form, a key that is absent leaves the property untouched, while a key that is present with undefined removes it; the type keeps undefined explicit so that exactOptionalPropertyTypes does not blur the two. The values parameter of withRawProperty() is required, so a forgotten argument is a type error rather than a silent removal.
  • The record and iterable forms are told apart at runtime by Symbol.iterator. Arrays and Maps take the iterable path. The record form is the expected common case.
  • Property IRIs are validated exactly as in the read functions, including the warning for schemes other than http: and https:. The raw-property-iri-invalid lint rule from Allow access to unsupported vocabulary properties #1272 should inspect the IRI argument and statically known entry keys.
  • A known IRI, including a redundantProperties alias, is canonicalized and decoded into the typed field, so one IRI never has two sources of truth. If two entries resolve to the same destination after canonicalization, the call is rejected. Whether known IRIs should instead be rejected outright is to be decided while implementing this.
  • An unknown IRI replaces the entry in the retained property map. The rest of the map is shared with the original object.
  • The result drops the cached JSON-LD and any signed representation retained by signObject(), and resets the trust state of the properties that changed. A proof over the original object does not cover the copy; sign it again if needed.

Serialization

The read side does not change serialization. For the write side to be useful, toJsonLd() has to emit retained unknown properties. Decide:

  • whether every object emits its retained unknown properties, including objects parsed from incoming JSON-LD and never modified, or only objects produced by these functions;
  • how retained properties interact with compaction, since their IRIs may have no term in the default context and stay as full IRIs, or need an extra context;
  • whether nested objects emit their own retained properties when the parent is serialized. The read side decided that encoding a parent's known property does not include the child's retained unknown properties, and this should be reconciled.

The first question changes outgoing JSON-LD for existing applications, so it may need its own changelog entry or a release note even if the answer is “yes”.

Documentation and tests

Extend the raw property section of docs/manual/vocab.md with a write example, such as attaching an extension property to an outgoing Note, and document the removal semantics and the effect on signatures. Tests should cover round trips through getRawPropertyValues(), alias canonicalization, duplicate destinations, removal in all three spellings, embedding the result in a parent and serializing the parent, and both entry forms.

Activity

  1. added theissue type on Nov 1, 2026
  2. dahlia commented on Nov 2, 2026

    @dahlia
    MemberAuthor

    The raw-property-iri-invalid rule in #1356 now checks only getRawPropertyValues() and getRawPropertyValue(). Support for withRawProperty() and withRawProperties() was removed during review so the rule does not lock in an argument shape before this API exists.

    The pull request for this issue should therefore extend the rule in packages/lint/src/lib/raw-property-iri.ts: add both names to the checked functions, check the IRI argument of withRawProperty(), and check the statically known keys of withRawProperties() in both the record form (including computed keys bound to a const) and the iterable form ([key, values] tuples in an array literal). The rule's documentation in docs/manual/lint.md and its tests need the same update.

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

    component/vocabActivity Vocabulary relatedcomponent/vocab-toolsVocabulary code generation (@fedify/vocab-tools)

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions