You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
One thing worries me: plain serde derives may not be enough here.
The hard part is not the Rust structs. It is the input shape ActivityPub permits. A sender can write an AS2 term as object or as https://www.w3.org/ns/activitystreams#object, and we need to treat those as the same property. Multi-valued fields come in as a scalar, an array, or nothing; the library should read all three the same way. Object slots have the same problem: the value is sometimes an embedded object, sometimes just the object's URI.
Fedify handles this in TypeScript through codegen from the Activity Vocabulary spec. The generated wrapper classes normalize these cases on read, so application code never has to remember every JSON-LD shape.
Rust probably gives us two decent approaches: proc macros that generate the serde glue, or build-time codegen from the vocabulary definitions.
Phase 1 does not strictly require this; mocked unit tests can sidestep it by constructing objects directly. That said, getting the shape right early would save some pain later, and it might be worth at least sketching the approach before feder-vocab solidifies.
#13 does not attempt full JSON-LD normalisation or expanded-term handling yet, so it still will not treat object and https://www.w3.org/ns/activitystreams#object as equivalent. I think that belongs in a later parser/normalisation layer rather than directly in the minimal vocab structs, especially if we want feder-vocab to stay no_std + alloc.
What this PR does cover is the lighter-weight shape normalization needed for the mocked Phase 1 flows:
Reference<T> handles object slots that are either an IRI or an embedded object.
OneOrMany<T> provides the scalar-or-array shape needed for future multi-valued fields.
Absence can be modeled as Option<OneOrMany<T>> on containing types.
The public tests exercise the ID-vs-object and scalar-vs-array shapes.
So this is intentionally a compact ActivityStreams profile, not full Fedify-style JSON-LD/codegen support yet. My expectation is that full expanded-term/context handling should be a later design task, possibly as a separate normalisation layer used by runtimes that can afford it. What do you think?
I agree with deferring full JSON-LD expansion, but I don't think the framing is quite right. A full JSON-LD processor isn't what we'd need to treat object and https://www.w3.org/ns/activitystreams#object as the same property. A lookup table mapping known AS2 expanded IRIs to their compact terms, applied to the raw JSON before serde sees it, would cover the cases we're likely to see. Mastodon, Misskey, and Pleroma all use the standard AS2 context and compact terms; payloads with expanded IRIs in the wild are rare enough that a fixed table probably handles the compatibility issue in practice without touching a proper JSON-LD processor at all. That layer could live outside feder-vocab and still keep the crate no_std.
On Reference<T> and OneOrMany<T>: the direction seems right, but Option<OneOrMany<T>> creates a problem. In JSON-LD, an empty array and an absent property mean the same thing, so there's no meaningful distinction between None and Some(Many([])). If we expose both, callers have to branch on a state JSON-LD doesn't actually have.
What I'd suggest instead is a References<T> type (plural, to signal “zero or more”) that handles the absent/scalar/array deserialization internally and serializes back by omitting the field when empty. Then multi-valued fields don't need an Option, and References carries the JSON-LD meaning in the name. Option<Reference<T>> still makes sense for optional singular fields like object or inReplyTo. So the split would be: Reference<T> for required singular, Option<Reference<T>> for optional singular, References<T> for anything that can appear zero or more times.
One serialization detail I'd decide explicitly: when References<T> contains exactly one element, does it serialize as a scalar or as a single-element array? Both appear in the wild, and some implementations are picky. I'd probably default to scalar and document that.
Activity
[-]Portable Core Skeleton: minimal ActivityPub model types[/-][+]Minimal ActivityPub model types[/+]dahlia commentedon May 26, 2026
One thing worries me: plain serde derives may not be enough here.
The hard part is not the Rust structs. It is the input shape ActivityPub permits. A sender can write an AS2 term as
objector ashttps://www.w3.org/ns/activitystreams#object, and we need to treat those as the same property. Multi-valued fields come in as a scalar, an array, or nothing; the library should read all three the same way. Object slots have the same problem: the value is sometimes an embedded object, sometimes just the object's URI.Fedify handles this in TypeScript through codegen from the Activity Vocabulary spec. The generated wrapper classes normalize these cases on read, so application code never has to remember every JSON-LD shape.
Rust probably gives us two decent approaches: proc macros that generate the serde glue, or build-time codegen from the vocabulary definitions.
Phase 1 does not strictly require this; mocked unit tests can sidestep it by constructing objects directly. That said, getting the shape right early would save some pain later, and it might be worth at least sketching the approach before
feder-vocabsolidifies.sij411 commentedon Jun 5, 2026
#13 does not attempt full JSON-LD normalisation or expanded-term handling yet, so it still will not treat
objectandhttps://www.w3.org/ns/activitystreams#objectas equivalent. I think that belongs in a later parser/normalisation layer rather than directly in the minimal vocab structs, especially if we wantfeder-vocabto stayno_std + alloc.What this PR does cover is the lighter-weight shape normalization needed for the mocked Phase 1 flows:
Reference<T>handles object slots that are either an IRI or an embedded object.OneOrMany<T>provides the scalar-or-array shape needed for future multi-valued fields.Option<OneOrMany<T>>on containing types.So this is intentionally a compact ActivityStreams profile, not full Fedify-style JSON-LD/codegen support yet. My expectation is that full expanded-term/context handling should be a later design task, possibly as a separate normalisation layer used by runtimes that can afford it. What do you think?
dahlia commentedon Jun 5, 2026
I agree with deferring full JSON-LD expansion, but I don't think the framing is quite right. A full JSON-LD processor isn't what we'd need to treat
objectandhttps://www.w3.org/ns/activitystreams#objectas the same property. A lookup table mapping known AS2 expanded IRIs to their compact terms, applied to the raw JSON before serde sees it, would cover the cases we're likely to see. Mastodon, Misskey, and Pleroma all use the standard AS2 context and compact terms; payloads with expanded IRIs in the wild are rare enough that a fixed table probably handles the compatibility issue in practice without touching a proper JSON-LD processor at all. That layer could live outsidefeder-vocaband still keep the crateno_std.On
Reference<T>andOneOrMany<T>: the direction seems right, butOption<OneOrMany<T>>creates a problem. In JSON-LD, an empty array and an absent property mean the same thing, so there's no meaningful distinction betweenNoneandSome(Many([])). If we expose both, callers have to branch on a state JSON-LD doesn't actually have.What I'd suggest instead is a
References<T>type (plural, to signal “zero or more”) that handles the absent/scalar/array deserialization internally and serializes back by omitting the field when empty. Then multi-valued fields don't need anOption, andReferencescarries the JSON-LD meaning in the name.Option<Reference<T>>still makes sense for optional singular fields likeobjectorinReplyTo. So the split would be:Reference<T>for required singular,Option<Reference<T>>for optional singular,References<T>for anything that can appear zero or more times.One serialization detail I'd decide explicitly: when
References<T>contains exactly one element, does it serialize as a scalar or as a single-element array? Both appear in the wild, and some implementations are picky. I'd probably default to scalar and document that.sij411 commentedon Jun 10, 2026
Thanks, I addressed this in #14