Verified against jsonld.js 8.x run against the live https://www.w3.org/ns/activitystreams context, JSON-LD 1.1, json-ld.org/playground, Mastodon v4.7.3 context_helper.rb on 2026-10-15.
Working with JSON-LD
This page is a guide. To check an object interactively, use the Activity Linter; for full JSON-LD processing, the JSON-LD Playground.
ActivityPub uses JSON-LD for semantic data representation. Understanding JSON-LD helps debug context issues and ensure interoperability.
Online Tools
W3C JSON-LD Playground
The official tool for testing JSON-LD:
Features:
- Expand, compact, flatten and frame operations
- Permalinks for sharing an example
Using the Playground
- Paste your ActivityPub object in the input
- Select operation (usually "Expand")
- View the expanded form to see resolved terms
JSON-LD Operations
Expansion
Expands compact terms to full IRIs:
Input:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello, world!"
}
Expanded:
[
{
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
"https://www.w3.org/ns/activitystreams#content": [
{"@value": "Hello, world!"}
]
}
]
Compaction
Shortens full IRIs using a context:
const jsonld = require('jsonld');
const expanded = [{
"@type": ["https://www.w3.org/ns/activitystreams#Note"],
"https://www.w3.org/ns/activitystreams#content": [
{"@value": "Hello!"}
]
}];
const context = {"@context": "https://www.w3.org/ns/activitystreams"};
const compacted = await jsonld.compact(expanded, context);
// Result: { "@context": "...", "type": "Note", "content": "Hello!" }
Common Contexts
ActivityStreams
{
"@context": "https://www.w3.org/ns/activitystreams"
}
Defines: type, id, actor, object, content, published, etc.
Security (for HTTP Signatures)
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1"
]
}
Adds: publicKey, publicKeyPem, owner
Mastodon Extensions
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"toot": "http://joinmastodon.org/ns#",
"Emoji": "toot:Emoji",
"discoverable": "toot:discoverable",
"sensitive": "as:sensitive"
}
]
}
Validation Script
Check Context Resolution
const jsonld = require('jsonld');
async function validateContext(object) {
try {
// Try to expand - will fail if context is invalid
const expanded = await jsonld.expand(object);
console.log('✅ Context is valid');
console.log('Expanded form:', JSON.stringify(expanded, null, 2));
return true;
} catch (error) {
console.error('❌ Context error:', error.message);
return false;
}
}
// Test
const note = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello!"
};
validateContext(note);
Verify Term Resolution
async function checkTerms(object, expectedTerms) {
const expanded = await jsonld.expand(object);
const flat = expanded[0] || {};
const results = {};
for (const term of expectedTerms) {
const asNs = `https://www.w3.org/ns/activitystreams#${term}`;
results[term] = asNs in flat || `@${term}` in flat;
}
return results;
}
// Check if common terms resolve
const terms = ['type', 'actor', 'object', 'content', 'published'];
const results = await checkTerms(activity, terms);
console.log(results);
// { type: true, actor: true, object: true, content: true, published: true }
Common Issues
Missing Context
// ❌ Bad - no context
{
"type": "Note",
"content": "Hello"
}
// ✅ Good - has context
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello"
}
Wrong Context URL
// ❌ Bad - typo in URL
{
"@context": "https://www.w3.org/ns/activitystream",
"type": "Note"
}
// ✅ Good - correct URL
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note"
}
Context Order Matters
// Later contexts override earlier ones
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"content": "http://example.com/custom#content"
}
],
"content": "This uses custom namespace"
}
Unknown Terms
The Activity Streams context sets "@vocab": "_:", so a term it doesn't define isn't dropped — it expands to a blank-node property:
const object = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"customField": "Not defined anywhere"
};
// jsonld.expand(object) gives "_:customField", and compacting brings back
// "customField" — but "_:customField" has no global meaning, so another
// server can't tell your customField from anyone else's.
Define extension terms in your own context (as Mastodon does with toot:) so they expand to a real IRI. With a context that has no @vocab, undefined terms are dropped on expansion.
Testing ActivityPub Objects
Validate a Note
async function validateNote(note) {
const errors = [];
// Check context
if (!note['@context']) {
errors.push('Missing @context');
}
// Expand and check required fields
try {
const expanded = await jsonld.expand(note);
const obj = expanded[0] || {};
const asNs = 'https://www.w3.org/ns/activitystreams#';
if (!obj['@type']?.includes(`${asNs}Note`)) {
errors.push('type must be Note');
}
if (!obj[`${asNs}content`]) {
errors.push('Missing content');
}
if (!obj[`${asNs}attributedTo`]) {
errors.push('Missing attributedTo');
}
} catch (e) {
errors.push(`JSON-LD error: ${e.message}`);
}
return errors;
}
Validate an Activity
async function validateActivity(activity) {
const errors = [];
const asNs = 'https://www.w3.org/ns/activitystreams#';
try {
const expanded = await jsonld.expand(activity);
const obj = expanded[0] || {};
// Check for actor
if (!obj[`${asNs}actor`]) {
errors.push('Activity must have actor');
}
// Check for object (most activities need it)
const type = obj['@type']?.[0];
const intransitive = [
`${asNs}Arrive`,
`${asNs}Travel`,
`${asNs}Question`
];
if (!obj[`${asNs}object`] && !intransitive.includes(type)) {
errors.push('Activity should have object');
}
} catch (e) {
errors.push(`JSON-LD error: ${e.message}`);
}
return errors;
}
Local Testing Setup
Node.js Setup
npm install jsonld
const jsonld = require('jsonld');
// Custom document loader: serve known contexts from local copies.
// Save the real documents first, e.g.
// curl -H 'Accept: application/ld+json' https://www.w3.org/ns/activitystreams > activitystreams.jsonld
const { readFileSync } = require('fs');
const load = (file) => JSON.parse(readFileSync(file, 'utf8'));
const contexts = {
'https://www.w3.org/ns/activitystreams': load('./activitystreams.jsonld'),
'https://w3id.org/security/v1': load('./security-v1.jsonld'),
};
const customLoader = async (url) => {
if (contexts[url]) {
return { contextUrl: null, document: contexts[url], documentUrl: url };
}
// Refuse anything else: fetching arbitrary context URLs from remote
// documents is an SSRF and DoS risk (see JSON-LD security)
throw new Error(`Context not allowed: ${url}`);
};
jsonld.documentLoader = customLoader;
For a production allowlisted loader, see JSON-LD security.
Browser Setup
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsonld/8.3.2/jsonld.min.js"></script>
<script>
async function test() {
const note = {
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Note",
"content": "Hello!"
};
const expanded = await jsonld.expand(note);
console.log(expanded);
}
test();
</script>
Quick Reference
┌────────────────────────────────────────────────────────────┐
│ JSON-LD QUICK REFERENCE │
├────────────────────────────────────────────────────────────┤
│ │
│ @context - Defines term mappings │
│ @id - Unique identifier (same as 'id' in AS2) │
│ @type - Object type (same as 'type' in AS2) │
│ @value - Literal value in expanded form │
│ │
│ Operations: │
│ • Expand - Resolve all terms to full IRIs │
│ • Compact - Shorten IRIs using context │
│ • Flatten - Remove nesting │
│ • Frame - Reshape to match template │
│ │
└────────────────────────────────────────────────────────────┘
Related Tools
- Actor Inspector - Inspect actors
- Troubleshooting by symptom - General debugging
- Compliance Checklist - Full validation