Skip to content

JSON-LD for HATEOAS — Semantic Hypermedia with Linked Data Contexts

DodaTech Updated 2026-06-28 5 min read

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

  1. Not serving the context document — the @context URL must be dereferenceable. If you reference a context URL, serve an actual JSON-LD document there.
  2. Using ambiguous property names — without a context, name could mean anything. Always provide a context that maps properties to well-known vocabularies.
  3. 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.
  4. Forgetting @id for link relations — link targets should be expressed as @id values, not embedded objects. This makes them dereferenceable resources.
  5. Ignoring existing vocabularies — don't create custom terms for things that already have definitions in schema.org or other well-known vocabularies.

Practice Questions

  1. What is the purpose of @context in JSON-LD?
  2. How does JSON-LD make APIs machine-readable?
  3. What is the difference between compact and expanded JSON-LD?
  4. How do you link to external vocabularies like schema.org?
  5. Why should @context URLs 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 is the difference between JSON-LD and HAL?

JSON-LD focuses on semantic meaning using linked data principles. HAL focuses on navigational links. JSON-LD answers 'what is this data?' HAL answers 'where can I go?'

Do I need a semantic web server for JSON-LD?

No. JSON-LD is just JSON with a @context property. Any REST framework can serve JSON-LD by adding the context to responses.

How does JSON-LD help with API versioning?

Properties are identified by IRIs, not names. If a property's IRI stays the same, the client understands it. You can change the JSON key while keeping the same semantic IRI.

Can JSON-LD replace OpenAPI documentation?

JSON-LD complements OpenAPI. OpenAPI describes endpoints and schemas. JSON-LD describes the meaning of individual properties, enabling automated understanding.

What is the Hydra Core vocabulary?

Hydra is a JSON-LD-based hypermedia vocabulary that adds hypermedia controls (links, actions, collections) to JSON-LD, similar to Siren but with semantic web foundations.

What's Next

Learn HATEOAS API design principles

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro