JSON-LD for HATEOAS — Semantic Hypermedia with Linked Data Contexts
In this tutorial, you will learn about JSON. We cover key concepts, practical examples, and best practices to help you master this topic.
JSON-LD (JSON for Linked Data) brings semantic web capabilities to REST APIs by using @context to map JSON properties to global identifiers (IRIs), enabling clients to understand the meaning of data beyond the API's documentation.
What You'll Learn
- JSON-LD context and vocabularies
- Using IRIs for property and type definitions
- Linking to external schemas (schema.org)
- JSON-LD for API discoverability
- Compact vs expanded document forms
- Hydra Core vocabulary for hypermedia
Why It Matters
JSON-LD makes your API machine-readable. A client can dereference the @context URL to discover the full schema of your API, understand what each property means, and find links to related resources. DodaTech's Durga Antivirus Pro uses JSON-LD for its public API, allowing third-party integrators to automatically understand and adapt to API changes.
Real-World Use
A third-party SIEM system reads the API's @context URL and discovers that the severity property maps to a known vocabulary with defined value ranges. When the API adds a new severity level, the SIEM system understands it without code changes.
flowchart LR
A["API Response\nJSON-LD"] --> B["@context: https://schema.dodatech.com"]
B --> C["schema.dodatech.com\n(threat, severity, device)"]
B --> D["schema.org\n(name, description, date)"]
A --> E["@type: Threat"]
A --> F["@id: /threats/101"]
A --> G["severity: critical"]
G --> H["Client dereferences @context\nDiscovers severity enum values\nunderstands 'critical'"]
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
Code Examples
Example 1: JSON-LD Resource with Context
{
"@context": {
"@vocab": "https://schema.dodatech.com/ns/",
"schema": "https://schema.org/",
"name": "schema:name",
"description": "schema:description",
"dateCreated": "schema:dateCreated",
"threat": {
"@id": "https://schema.dodatech.com/ns/Threat",
"@type": "@id"
},
"severity": {
"@id": "https://schema.dodatech.com/ns/severity",
"@type": "https://schema.org/Text"
},
"device": {
"@id": "https://schema.dodatech.com/ns/device",
"@type": "@id"
},
"critical": "https://schema.dodatech.com/ns/critical",
"high": "https://schema.dodatech.com/ns/high",
"medium": "https://schema.dodatech.com/ns/medium"
},
"@id": "/threats/101",
"@type": "threat",
"name": "Ransomware-X",
"description": "A critical ransomware variant detected on Office-PC",
"severity": "critical",
"dateCreated": "2026-06-28T10:00:00Z",
"device": "/devices/1",
"threat": "/threats/101/analysis"
}
Example 2: JSON-LD Server Implementation
const express = require('express');
const app = express();
const CONTEXT_URL = 'https://schema.dodatech.com/context.jsonld';
const context = {
'@context': {
'@vocab': 'https://schema.dodatech.com/ns/',
'schema': 'https://schema.org/',
'name': 'schema:name',
'description': 'schema:description',
'dateCreated': 'schema:dateCreated',
'severity': {
'@id': 'https://schema.dodatech.com/ns/severity',
},
'device': { '@type': '@id' },
'threat': { '@type': '@id' },
},
};
// Serve the context document
app.get('/context.jsonld', (req, res) => {
res.json({
'@context': {
...context['@context'],
// Add vocabulary documentation
'@vocab': 'https://schema.dodatech.com/ns/',
'threat': 'https://schema.dodatech.com/ns/Threat',
'device': 'https://schema.dodatech.com/ns/Device',
'severity': {
'@id': 'https://schema.dodatech.com/ns/severity',
'@range': 'xsd:string',
'critical': 'https://schema.dodatech.com/ns/critical',
'high': 'https://schema.dodatech.com/ns/high',
'medium': 'https://schema.dodatech.com/ns/medium',
'low': 'https://schema.dodatech.com/ns/low',
},
},
});
});
function jsonldResource(id, type, properties, links) {
const resource = {
'@context': CONTEXT_URL,
'@id': id,
'@type': type,
...properties,
};
// Add links as properties with @id type
for (const [rel, href] of Object.entries(links)) {
resource[rel] = href;
}
return resource;
}
app.get('/threats/:id', (req, res) => {
const threat = getThreat(req.params.id);
res.json(jsonldResource(
`/threats/${threat.id}`,
'threat',
{
name: threat.name,
description: threat.description,
severity: threat.severity,
dateCreated: threat.createdAt,
},
{
device: `/devices/${threat.deviceId}`,
analysis: `/threats/${threat.id}/analysis`,
},
));
});
// Collection with JSON-LD
app.get('/threats', (req, res) => {
const threats = getAllThreats();
res.json(jsonldResource('/threats', 'Collection', {
name: 'Threat Collection',
total: threats.length,
items: threats.map(t => ({
'@id': `/threats/${t.id}`,
'@type': 'threat',
name: t.name,
severity: t.severity,
})),
}));
});
Example 3: JSON-LD Client with Semantic Understanding
class JsonLdClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.contextCache = new Map();
}
async fetch(url) {
const res = await fetch(`${this.baseUrl}${url}`);
return res.json();
}
async resolveContext(doc) {
if (!doc['@context']) return doc;
let context = doc['@context'];
if (typeof context === 'string') {
// Fetch remote context
if (!this.contextCache.has(context)) {
const res = await fetch(context);
const ctx = await res.json();
this.contextCache.set(context, ctx['@context'] || ctx);
}
context = this.contextCache.get(context);
}
// Expand properties using context
const expanded = { ...doc };
delete expanded['@context'];
for (const [key, value] of Object.entries(doc)) {
if (key.startsWith('@')) continue;
// Look up property meaning in context
const def = context[key];
if (typeof def === 'string') {
console.log(`Property '${key}' maps to: ${def}`);
} else if (def && def['@id']) {
console.log(`Property '${key}' has IRI: ${def['@id']}`);
if (def['@type'] === '@id' && typeof value === 'string') {
// This is a link to another resource
expanded[`_link_${key}`] = value;
}
}
}
return expanded;
}
async navigate() {
// Fetch threat and understand its structure semantically
const raw = await this.fetch('/threats/101');
const expanded = await this.resolveContext(raw);
console.log('Resource type:', expanded['@type']);
console.log('Properties:', Object.keys(expanded)
.filter(k => !k.startsWith('@')));
// Follow semantic links
if (expanded.device) {
console.log(`Linked to device: ${expanded.device}`);
const device = await this.fetch(expanded.device);
console.log('Device:', device.name);
}
// Severity is typed - client understands the values
const severity = expanded.severity;
if (severity === 'critical' || severity === 'high') {
console.log('Alert: High severity threat detected');
}
}
}
Common Mistakes
- Not serving the context document — the
@contextURL must be dereferenceable. If you reference a context URL, serve an actual JSON-LD document there. - Using ambiguous property names — without a context,
namecould mean anything. Always provide a context that maps properties to well-known vocabularies. - Mixing JSON-LD with non-semantic formats — if you serve JSON-LD, the entire API should be consistent. Don't serve JSON-LD for some endpoints and plain JSON for others.
- Forgetting @id for link relations — link targets should be expressed as
@idvalues, not embedded objects. This makes them dereferenceable resources. - Ignoring existing vocabularies — don't create custom terms for things that already have definitions in schema.org or other well-known vocabularies.
Practice Questions
- What is the purpose of
@contextin JSON-LD? - How does JSON-LD make APIs machine-readable?
- What is the difference between compact and expanded JSON-LD?
- How do you link to external vocabularies like schema.org?
- Why should
@contextURLs be dereferenceable?
Challenge: Design a JSON-LD context for a threat management API with vocabulary terms for threat, device, scan, severity, and status. Map properties to schema.org where possible and create custom terms only where necessary.
Mini Project
Build a JSON-LD API for a threat management system with: dereferenceable context document, semantic property mapping using schema.org and custom vocabulary, JSON-LD collections with embedded items, link relations using @id, and a client that resolves contexts to understand the API structure.
FAQ
What's Next
Learn HATEOAS API design principles
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro