ActivityStreams is the fediverse’s base data model. It’s why we have things like Actors, Objects and Activities. It models social interactions as an actor-based system. ActivityStreams is canonically exchanged in JSON-LD compacted document form, which is JSON with some special keys for the “linked data” aspect.

JSON-LD has two(-ish) forms:

  • Compacted document form, which looks like your typical JSON.
  • Expanded document form, which looks more like a tree of nodes.

People tend to prefer compacted form because it looks more like the JSON payloads you run every day. Expanded form is often characterised as too repetitive and verbose.

Expanded document form

Here is what it looks like:

[
  {
    "https://www.w3.org/ns/activitystreams#actor": [
      {
        "https://www.w3.org/ns/activitystreams#name": [
          {
            "@value": "Sally"
          }
        ],
        "@type": [
          "https://www.w3.org/ns/activitystreams#Person"
        ]
      }
    ],
    "https://www.w3.org/ns/activitystreams#object": [
      {
        "https://www.w3.org/ns/activitystreams#content": [
          {
            "@value": "This is a simple note"
          }
        ],
        "https://www.w3.org/ns/activitystreams#name": [
          {
            "@value": "A Simple Note"
          }
        ],
        "@type": [
          "https://www.w3.org/ns/activitystreams#Note"
        ]
      }
    ],
    "https://www.w3.org/ns/activitystreams#summary": [
      {
        "@value": "Sally created a note"
      }
    ],
    "@type": [
      "https://www.w3.org/ns/activitystreams#Create"
    ]
  }
]

Is this verbose? Heck yes. But notice how regular it is? Special keys have an @ prefix. Attributes use URIs for keys. Everything is a list of object, except for some of the keyword keys. Once you’ve seen one payload, you’ve basically seen them all.

There are a few more keywords, but that’s basically it. Many of the keywords aren’t present in expanded form. This is trivial to parse in any language, and can be correctly generated with string templating if you need to. Want to extend it with your own attribute, and not risk colliding with an existing key? No problem, all non-keyword attributes are namespaced so bring your own!

Compacted document form

The equivalent of the expanded document form is:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "summary": "Sally created a note",
  "type": "Create",
  "actor": {
    "type": "Person",
    "name": "Sally"
  },
  "object": {
    "type": "Note",
    "name": "A Simple Note",
    "content": "This is a simple note"
  }
}

Looks simpler right? Suddenly our keys are single-valued which is easier. No wrapping objects where you didn’t expect them either. All gone. But this hides a problem. Notice how in expanded document form name is an array? That’s because it can have more than one value. Because it’s JSON-LD, just about every attribute is multi-valued.

JSON-LD doesn’t let you describe an attribute as being singular, it’s not part of its data model. ActivityStreams constrains some attributes by calling them “functional” in the specification, meaning they are singular. But that information is lost to non-JSON-LD aware processors, and wouldn’t be known to anyone who hasn’t read the spec in enough detail.

Even if you read the ActivityStreams spec, the examples tend to show the compacted form with most attributes being singular, so it doesn’t really clue you in to the situation. The only way to know that this is the case, and the consequences that come with it, is to be aware enough of JSON-LD to begin with.

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "summary": "Sally created a note",
  "type": "Create",
  "actor": {
    "type": "Person",
    "name": "Sally"
  },
  "object": {
    "type": "Note",
    "name": ["A Simple Note", "And now there were two"],
    "content": "This is a simple note"
  }
}

This is perfectly legitimate, but now the name fields has two possible shapes you have to deal with. This becomes worse, because name can also be a singular object (like actor), an array of objects, or an array of string and objects. How it becomes any of these things is dictated by the terms defined in the @context and the compaction algorithm.

Semantic compression

To take an expanded document and transform it into the compacted form, we use the @context and the terms it defines to shorten key names and reduce the values to their simplest representation. We’re compressing the object, using a shared dictionary and a standardised algorithm. In the above example, the context is a URI. You can retrieve the context document from that URI which contains the term definitions.

This semantic compression contributes to the vast, vast, vast majority of the complexity in JSON-LD. At least as it relates to ActivityStreams and the fediverse. It also has a corralary, which is expansion or decompression. Expansion is much easier, but it’s still quite a bit of work to get right. The official JSON-LD test suite for compaction and expansion has a lot of cases, on top of all the context processing ones. The context processing is also very important. Interpret the context wrongly, and you’ll compact or expand to the wrong thing.

If you’re a JSON-LD aware processor, this isn’t a big issue. You get compacted form, you use whatever library is available in your language to expand it and then you work on the expanded form. Once you’re ready to fax it to a different system, you typically compact it. For ActivityStreams we always use the compacted form between systems.

But if you’re a JSON-LD unaware processor, you need to deal with all the various shapes compacted document form can come in. Forgetting to deal with one will likely result in bugs. Not every language has a good JSON-LD library available, and not everyone wants to take on a dependency lie that. After all, the spec says you can treat it as plain JSON.

This is why in practice the fediverse uses a more restrictive subset of “shapes” than what JSON-LD allows. Some are restricted by ActivityStreams, some just by convention because not doing so breaks federation with parts of the ecosystem. We lose a lot of the power, and sometimes extensibility, we could get from using JSON-LD properly because we’re half-assing it.

Not expanding documents first can also be dangerous. The context can change the semantics of a document when going from compacted to expanded form and back. It can alias keys to completely unexpected things. For example, you could compact the https://example.com/ns#privateKey attribute to a field called publicKey. You shouldn’t, but malicious contexts can cause shenanigans. This sort of JSON-LD unaware processing of compacted document form has caused a number of security issues on the fedi, and it’s part of why we don’t use Linked Data Signatures anymore.

BUT THE BYTES

Yeap, I know. But notice how I talked about semantic compression before? That’s because we also have other forms of compression. Over time we’ve gotten really good at having computers compress text for us. JSON is text. LZMA(2), Gzip, Bzip2, Brotli, Snappy, Zstandard etc. They can all “compact” expanded document form too.

This type of compression doesn’t change the semantics or shape of the document. It just shrinks the document in transit, or for storage. Compression libraries are also extremely good at doing this quickly and in parallel, and often have different compression levels you can choose from to trade-off CPU and memory for more or less compression.

Text-basend compression works exceptionally well for expanded document form because of how much token repition there is in a document. Its arguably verbose nature makes it a great target. Here are some numbers, based on a post/toot/tweet of 500 characters and a single hashtag.

  • Compacted document form
    {
      "@context": [
        "https://www.w3.org/ns/activitystreams",
        {
          "ostatus": "http://ostatus.org#",
          "atomUri": "ostatus:atomUri",
          "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
          "conversation": "ostatus:conversation",
          "sensitive": "as:sensitive",
          "toot": "http://joinmastodon.org/ns#",
          "quote": "https://w3id.org/fep/044f#quote",
          "quoteUri": "http://fedibird.com/ns#quoteUri",
          "_misskey_quote": "https://misskey-hub.net/ns#_misskey_quote",
          "quoteAuthorization": {
            "@id": "https://w3id.org/fep/044f#quoteAuthorization",
            "@type": "@id"
          },
          "manualApproval": {
            "@id": "gts:manualApproval",
            "@type": "@id"
          },
          "Hashtag": "as:Hashtag"
        }
      ],
      "id": "https://example.com/users/Alice/statuses/115645813940637553#updates/1764753234",
      "type": "Create",
      "actor": "https://example.com/users/Alice",
      "to": [
        "https://www.w3.org/ns/activitystreams#Public"
      ],
      "object": {
        "id": "https://example.com/users/Alice/statuses/115645813940637553",
        "type": "Note",
        "summary": null,
        "inReplyTo": null,
        "published": "2025-12-01T18:51:20Z",
        "url": "https://example.com/@Alice/115645813940637553",
        "attributedTo": "https://example.com/users/Alice",
        "to": [
          "https://www.w3.org/ns/activitystreams#Public"
        ],
        "cc": [
          "https://example.com/users/Alice/followers"
        ],
        "sensitive": false,
        "atomUri": "https://example.com/users/Alice/statuses/115645813940637553",
        "inReplyToAtomUri": null,
        "conversation": "tag:example.com,2025-12-01:objectId=629221833:objectType=Conversation",
        "context": null,
        "content": "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or. <a href=\"https://example.com/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\"><span>fediverse</span></a>",
        "contentMap": {
          "en":"But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or. <a href=\"https://example.com/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\"><span>fediverse</span></a>"
        },
        "attachment": [],
        "tag": [
          {
            "type": "Hashtag",
            "href": "https://example.com/tags/fediverse",
            "name": "#fediverse"
          }
        ],
        "replies": {
          "id": "https://example.com/users/Alice/statuses/115645813940637553/replies",
          "type": "Collection",
          "first": {
            "type": "CollectionPage",
            "next": "https://example.com/users/Alice/statuses/115645813940637553/replies?min_id=115649989327026376&page=true",
            "partOf": "https://example.com/users/Alice/statuses/115645813940637553/replies",
            "items": [
              "https://example.com/users/Alice/statuses/115649989327026376"
            ]
          }
        }
      }
    }
    
  • Expanded document form
    [
      {
        "https://www.w3.org/ns/activitystreams#actor": [
          {
            "@id": "https://example.com/users/Alice"
          }
        ],
        "@id": "https://example.com/users/Alice/statuses/115645813940637553#updates/1764753234",
        "https://www.w3.org/ns/activitystreams#object": [
          {
            "http://ostatus.org#atomUri": [
              {
                "@value": "https://example.com/users/Alice/statuses/115645813940637553"
              }
            ],
            "https://www.w3.org/ns/activitystreams#attachment": [],
            "https://www.w3.org/ns/activitystreams#attributedTo": [
              {
                "@id": "https://example.com/users/Alice"
              }
            ],
            "https://www.w3.org/ns/activitystreams#cc": [
              {
                "@id": "https://example.com/users/Alice/followers"
              }
            ],
            "https://www.w3.org/ns/activitystreams#content": [
              {
                "@value": "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or. <a href=\"https://example.com/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\"><span>fediverse</span></a>"
              },
              {
                "@value": "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or. <a href=\"https://example.com/tags/fediverse\" class=\"mention hashtag\" rel=\"tag\"><span>fediverse</span></a>",
                "@language": "en"
              }
            ],
            "http://ostatus.org#conversation": [
              {
                "@value": "tag:example.com,2025-12-01:objectId=629221833:objectType=Conversation"
              }
            ],
            "@id": "https://example.com/users/Alice/statuses/115645813940637553",
            "https://www.w3.org/ns/activitystreams#published": [
              {
                "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
                "@value": "2025-12-01T18:51:20Z"
              }
            ],
            "https://www.w3.org/ns/activitystreams#replies": [
              {
                "https://www.w3.org/ns/activitystreams#first": [
                  {
                    "https://www.w3.org/ns/activitystreams#items": [
                      {
                        "@id": "https://example.com/users/Alice/statuses/115649989327026376"
                      }
                    ],
                    "https://www.w3.org/ns/activitystreams#next": [
                      {
                        "@id": "https://example.com/users/Alice/statuses/115645813940637553/replies?min_id=115649989327026376&page=true"
                      }
                    ],
                    "https://www.w3.org/ns/activitystreams#partOf": [
                      {
                        "@id": "https://example.com/users/Alice/statuses/115645813940637553/replies"
                      }
                    ],
                    "@type": [
                      "https://www.w3.org/ns/activitystreams#CollectionPage"
                    ]
                  }
                ],
                "@id": "https://example.com/users/Alice/statuses/115645813940637553/replies",
                "@type": [
                  "https://www.w3.org/ns/activitystreams#Collection"
                ]
              }
            ],
            "https://www.w3.org/ns/activitystreams#sensitive": [
              {
                "@value": false
              }
            ],
            "https://www.w3.org/ns/activitystreams#tag": [
              {
                "https://www.w3.org/ns/activitystreams#href": [
                  {
                    "@id": "https://example.com/tags/fediverse"
                  }
                ],
                "https://www.w3.org/ns/activitystreams#name": [
                  {
                    "@value": "#fediverse"
                  }
                ],
                "@type": [
                  "https://www.w3.org/ns/activitystreams#Hashtag"
                ]
              }
            ],
            "https://www.w3.org/ns/activitystreams#to": [
              {
                "@id": "https://www.w3.org/ns/activitystreams#Public"
              }
            ],
            "@type": [
              "https://www.w3.org/ns/activitystreams#Note"
            ],
            "https://www.w3.org/ns/activitystreams#url": [
              {
                "@id": "https://example.com/@Alice/115645813940637553"
              }
            ]
          }
        ],
        "https://www.w3.org/ns/activitystreams#to": [
          {
            "@id": "https://www.w3.org/ns/activitystreams#Public"
          }
        ],
        "@type": [
          "https://www.w3.org/ns/activitystreams#Create"
        ]
      }
    ]
    

Uncompressed

  • Compacted form: 3638 bytes.
  • Expanded form: 5151 bytes.

The semantically compressed document has an advantage at this stage.

Compressed

Lets look at what happens when we compress both documents with gzip and Zstandard. This is without the use of specially tuned compression dictionaries. They’re simple invocation of the gzip and zstd CLIs, varying the compression level.

Compacted form

GzipZstandard
Level Bytes Ratio
1 1223 2.97
2 1188 3.06
3 1183 3.07
4 1116 3.25
5 1102 3.30
6 1100 3.30
7-9 1096 3.31
 
 
 
 
 
 
Level Bytes Ratio
1 1160 3.13
2 1187 3.06
3 1166 3.11
4 1112 3.27
5 1102 3.30
6 1101 3.30
7-11 1101 3.30
12 1090 3.33
13 1088 3.34
14 1089 3.33
15 1085 3.35
16 1083 3.35
17-22 1082 3.36

Expanded form

GzipZstandard
Level Bytes Ratio
1 1184 4.35
2 1156 4.45
3 1124 4.58
4 1071 4.80
5 1043 4.93
6 1031 4.99
7 1026 5.01
8 1022 5.03
9 1022 5.03
 
 
 
 
 
 
 
 
Level Bytes Ratio
1 1153 4.46
2 1177 4.37
3 1139 4.52
4 1059 4.86
5 1031 4.99
6 1021 5.04
7 1023 5.03
8 1016 5.06
9 1022 5.03
10 1016 5.06
11 1023 5.03
12 1013 5.08
13 1014 5.07
14 1001 5.14
15 1000 5.15
16 1011 5.09
17-22 1001 5.14

Results

Compacted form does still compress decently. But expanded form compresses much better and results in smaller payloads at every compression level. However, text-based compression requires no effort on the part of ActivityPub and ActivityStreams implementors. HTTP clients and servers can handle this transparently for you, and any implementation of note at least supports Gzip out of the box.

One other thing to point out is that in expanded document form, nothing unnecessary is retained. Any null attribute is gone. Empty arrays can be retained. In many cases implementations send a much larger @context than is strictly necessary for the expansion of the attached document. It will contain local term definitions that aren’t used.

That’s usually because implementations store a few contexts and use those. You can do some context minimisation at compaction time, but how to do so and do so correctly, especially in the face of embedded contexts in other attributes, is not specified. And of course, for non JSON-LD aware processors, they just plonk an @context field in it based on whatever they’ve seen other implementations do.

Conclusion

Text-based compression gives us smaller payloads with none of the added complexity of dealing with compacted document form. Since ActivityPub is standardised as exchanging ActivityStreams, we don’t benefit from using the compacted form just for looks. You only ever look at the JSON payloads when you’re developing or debugging a feature. The amount of people that do that is a tiny subset of the amount of people that are on the fedi. We can empower developers with better debugging tools instead of higher implementation complexity.

Aside from smaller payloads when compressed, using expanded form we can achieve the specification’s goal of treating ActivityStreams as “just JSON,” completely unaware of Linked Data and its semantics. It’s much easier to generate correctly, and much easier to parse correctly. It’s also much easier to handle in a streaming manner.

For anyone who cares about Linked Data or wishes to use it, they still can. You don’t lose any capability, but you drastically reduce complexity. You also make it blindingly obvious how to extend a document with your own attributes. Since the expanded document is itself an array of objects, we can transfer multiple activities in one go as well. That would let us achieve better compression ratios, and perhaps reduce some of the chattiness together with shared inbox delivery.

If we want the fediverse to shine, we should strive to lower the bar to entry as much as possible. Compacted document form doesn’t help with that. It creates problems we don’t need to have, it splits the ecosystem in two, and its semantical compression is worse at payload size reduction than conventional compression methods.

I hope that if the Social WG has a reboot and starts to consider a major revision of the specification, it will also end up standardising on expanded document form instead. I recognise that would be a big change. I think it’s possible to do so, especially since some of the larger implementations on fedi are already JSON-LD aware. I can also envision some kind of proxy or adjacent service that you could submit a compacted document to and get the expanded one in return, and vice-versa, to support this transition. That should ideally be something that could be embedded into existing servers, and WASM might provide us with a viable way to do just that.