This document defines an algorithm for converting JSON-LD 1.1 Frames into JSON Schema documents. The algorithm provides a systematic way to translate JSON-LD framing concepts into JSON Schema validation constructs, enabling validation of JSON-LD documents based on their expected frame structure.
This is an unofficial specification documenting the JSON-LD Frame to JSON Schema conversion algorithm. Comments and feedback are welcome via the GitHub repository.
JSON-LD 1.1 Framing [[JSON-LD11-FRAMING]] provides a mechanism to shape and structure JSON-LD data according to developer-specified patterns. JSON Schema [[JSON-SCHEMA]] provides a vocabulary for validating the structure, types, and constraints of JSON documents. This specification defines a formal algorithm for converting frames into schemas, bridging these two technologies.
The conversion algorithm enables several important use cases:
The algorithm is designed with the following goals:
@context definitionsA conforming implementation MUST implement the algorithm as specified in the Algorithm Definition section.
The following terms are used throughout this specification:
@embed, @explicit,
@requireAll, and @omitDefault@type
keyword within a context definitionThe conversion process consists of the following phases:
@context if presentThe algorithm has:
The main conversion function takes a frame and returns a schema.
frameToSchema(frame, schemaVersion, graphOnly)
Arguments:
frame: A JSON-LD Frame object
schemaVersion: JSON Schema version URI (optional, defaults to Draft 2020-12)
graphOnly: Boolean flag to output only the schema for @graph items (optional, defaults to false)
Returns:
A JSON Schema document
Steps:
1. Let frameContent be frame
2. If frame["@graph"] exists:
1. Let graphValue be frame["@graph"]
2. If graphValue is an array and its length is greater than 0:
1. Set frameContent to graphValue[0]
3. Else if graphValue is an object:
1. Set frameContent to graphValue
4. If frameContent["@context"] does not exist and frame["@context"] exists:
1. Create a new object merging frame["@context"] with frameContent
2. Set frameContent to this merged object
3. Let graphItemSchema be a new object
4. Set graphItemSchema["type"] to "object"
5. Let flags be the result of calling extractFramingFlags(frameContent)
6. Let context be the result of calling extractContext(frameContent)
7. Call processFrameObject(frameContent, graphItemSchema, flags, context)
8. If graphOnly is true:
1. Let schema be a new object
2. Set schema["$schema"] to schemaVersion
3. Merge all properties from graphItemSchema into schema
4. Return schema
9. Let schema be a new object
10. Set schema["$schema"] to schemaVersion
11. Set schema["type"] to "object"
12. Let properties be a new object
13. Set properties["@context"] to an empty object
14. Let graphProperty be a new object
15. Set graphProperty["type"] to "array"
16. Set graphProperty["items"] to graphItemSchema
17. Set properties["@graph"] to graphProperty
18. Set schema["properties"] to properties
19. Set schema["required"] to ["@context", "@graph"]
20. Set schema["additionalProperties"] to true
21. Return schema
This function extracts framing flags from a frame object.
extractFramingFlags(frame)
Arguments:
frame: A JSON-LD Frame object
Returns:
An object containing framing flag values
Steps:
1. Let flags be a new object
2. Set flags["embed"] to frame["@embed"] if present, otherwise true
3. Set flags["explicit"] to frame["@explicit"] if present, otherwise false
4. Set flags["requireAll"] to frame["@requireAll"] if present, otherwise false
5. Set flags["omitDefault"] to frame["@omitDefault"] if present, otherwise false
6. Return flags
This function extracts type information from the JSON-LD context.
extractContext(frame)
Arguments:
frame: A JSON-LD Frame object
Returns:
A map from property names to type URIs
Steps:
1. If frame["@context"] does not exist, return an empty map
2. Let context be frame["@context"]
3. Return the result of calling parseContext(context)
parseContext(context)
Arguments:
context: A JSON-LD context (string, object, or array)
Returns:
A map from property names to type URIs
Steps:
1. Let typeMap be a new empty map
2. If context is not an object, return typeMap
3. For each key → value in context:
1. If key starts with "@", continue to next iteration
2. If value is not an object, continue to next iteration
3. If value["@type"] exists:
1. Set typeMap[key] to value["@type"]
4. Return typeMap
This function processes a frame object and populates the corresponding schema object.
processFrameObject(frame, schema, flags, context)
Arguments:
frame: A JSON-LD Frame object
schema: The schema object to populate
flags: Framing flags
context: Type context mapping
Steps:
1. Let properties be a new empty object
2. Let required be a new empty array
3. If frame["@type"] exists:
1. Let typeSchema be the result of calling processTypeConstraint(frame["@type"])
2. Set properties["@type"] to typeSchema
3. If frame["@type"] is not a wildcard, append "@type" to required
4. If frame["@id"] exists:
1. Let idSchema be the result of calling processIdConstraint(frame["@id"])
2. Set properties["@id"] to idSchema
3. If frame["@id"] is not empty, append "@id" to required
5. For each key → value in frame:
1. If key is a framing keyword, continue to next iteration
2. If key is "@type" or "@id", continue to next iteration
3. Let propSchema be the result of calling processProperty(key, value, flags, context)
4. Set properties[key] to propSchema
5. If shouldBeRequired(key, value, flags) is true, append key to required
6. If properties is not empty, set schema["properties"] to properties
7. If required is not empty, set schema["required"] to required
8. If flags["explicit"] is true:
1. Set schema["additionalProperties"] to false
9. Otherwise:
1. Set schema["additionalProperties"] to true
processTypeConstraint(typeValue)
Arguments:
typeValue: The @type value from the frame
Returns:
A JSON Schema type constraint object
Steps:
1. If typeValue is a string:
1. Return { "const": typeValue }
2. If typeValue is an array:
1. If the length of typeValue is 0:
1. Return { "type": "string" }
2. If the length of typeValue is 1:
1. Return { "const": typeValue[0] }
3. Otherwise:
1. Return { "enum": typeValue }
3. If typeValue is an empty object:
1. Return { "type": "string" }
4. Return { "type": "string" }
processIdConstraint(idValue)
Arguments:
idValue: The @id value from the frame
Returns:
A JSON Schema ID constraint object
Steps:
1. If idValue is a string:
1. Return { "const": idValue }
2. If idValue is an empty object:
1. Return { "type": "string", "format": "uri" }
3. If idValue is an object and idValue["@id"] exists:
1. Return the result of calling processIdConstraint(idValue["@id"])
4. Return { "type": "string", "format": "uri" }
processProperty(key, value, flags, context)
Arguments:
key: Property name
value: Property value from frame
flags: Framing flags
context: Type context mapping
Returns:
A JSON Schema property definition
Steps:
1. Let contextType be context[key] if it exists, otherwise null
2. If value is an empty object:
1. Return the result of calling inferTypeFromContext(key, contextType)
3. If value is a string, number, or boolean:
1. Let jsonType be the result of calling inferJsonType(value)
2. Return { "type": jsonType, "default": value }
4. If value is an array:
1. Return the result of calling processArrayFrame(value, flags, context)
5. If value is an object:
1. Return the result of calling processNestedFrame(value, flags, context)
6. Return an empty object
inferTypeFromContext(key, contextType)
Arguments:
key: Property name
contextType: Type from context or null
Returns:
A JSON Schema type definition
Steps:
1. If contextType is null:
1. Return { "type": "string" }
2. Let mapping be the type mapping for contextType:
- "@id" → { "type": "string", "format": "uri" }
- "http://www.w3.org/2001/XMLSchema#string" → { "type": "string" }
- "http://www.w3.org/2001/XMLSchema#integer" → { "type": "integer" }
- "http://www.w3.org/2001/XMLSchema#int" → { "type": "integer" }
- "http://www.w3.org/2001/XMLSchema#long" → { "type": "integer" }
- "http://www.w3.org/2001/XMLSchema#boolean" → { "type": "boolean" }
- "http://www.w3.org/2001/XMLSchema#double" → { "type": "number" }
- "http://www.w3.org/2001/XMLSchema#float" → { "type": "number" }
- "http://www.w3.org/2001/XMLSchema#decimal" → { "type": "number" }
- "http://www.w3.org/2001/XMLSchema#dateTime" → { "type": "string", "format": "date-time" }
- "http://www.w3.org/2001/XMLSchema#date" → { "type": "string", "format": "date" }
- "http://www.w3.org/2001/XMLSchema#time" → { "type": "string", "format": "time" }
3. If mapping exists for contextType, return mapping
4. Return { "type": "string" }
processArrayFrame(arrayValue, flags, context)
Arguments:
arrayValue: Array from frame
flags: Framing flags
context: Type context mapping
Returns:
A JSON Schema array definition
Steps:
1. If the length of arrayValue is 0:
1. Return { "type": "array", "items": {} }
2. Let itemFrame be arrayValue[0]
3. Let itemSchema be determined as follows:
1. If itemFrame is an object:
1. Call processNestedFrame(itemFrame, flags, context)
2. Otherwise:
1. Let jsonType be the result of calling inferJsonType(itemFrame)
2. Use { "type": jsonType }
4. Return { "type": "array", "items": itemSchema }
processNestedFrame(frameObj, flags, context)
Arguments:
frameObj: Nested frame object
flags: Parent framing flags
context: Type context mapping
Returns:
A JSON Schema nested object definition
Steps:
1. Let nestedFlags be a new object with the following properties:
- "embed": frameObj["@embed"] if present, otherwise flags["embed"]
- "explicit": frameObj["@explicit"] if present, otherwise flags["explicit"]
- "requireAll": frameObj["@requireAll"] if present, otherwise flags["requireAll"]
- "omitDefault": frameObj["@omitDefault"] if present, otherwise flags["omitDefault"]
2. Let embedValue be nestedFlags["embed"]
3. If embedValue is false or "@never":
1. Return {
"oneOf": [
{ "type": "string", "format": "uri" },
{
"type": "object",
"properties": {
"@id": { "type": "string", "format": "uri" }
},
"required": ["@id"],
"additionalProperties": false
}
]
}
4. Let nestedSchema be { "type": "object" }
5. Call processFrameObject(frameObj, nestedSchema, nestedFlags, context)
6. Return nestedSchema
shouldBeRequired(key, value, flags)
Arguments:
key: Property name
value: Property value from frame
flags: Framing flags
Returns:
true if the property should be required, false otherwise
Steps:
1. If flags["requireAll"] is true:
1. Return true
2. If flags["omitDefault"] is true:
1. Return false
3. If value is an empty object:
1. Return true
4. If value is an object or array:
1. Return true
5. Return false
inferJsonType(value)
Arguments:
value: A JSON value
Returns:
A string representing the JSON type
Steps:
1. If value is a boolean, return "boolean"
2. If value is an integer, return "integer"
3. If value is a number, return "number"
4. If value is a string, return "string"
5. If value is an array, return "array"
6. If value is an object, return "object"
7. If value is null, return "null"
8. Return "string"
Given the following frame:
{
"@context": {
"name": "http://schema.org/name",
"age": {
"@id": "http://schema.org/age",
"@type": "http://www.w3.org/2001/XMLSchema#integer"
}
},
"@type": "Person",
"name": {},
"age": {}
}
The algorithm produces the following schema (default behavior with graphOnly=false):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@context": {},
"@graph": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": { "const": "Person" },
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["@type", "name", "age"],
"additionalProperties": true
}
}
},
"required": ["@context", "@graph"],
"additionalProperties": true
}
Given the following frame with nested objects and the @explicit flag:
{
"@type": "Person",
"@explicit": true,
"name": {},
"address": {
"@type": "PostalAddress",
"streetAddress": {},
"addressLocality": {}
}
}
The algorithm produces (default behavior with graphOnly=false):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@context": {},
"@graph": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": { "const": "Person" },
"name": { "type": "string" },
"address": {
"type": "object",
"properties": {
"@type": { "const": "PostalAddress" },
"streetAddress": { "type": "string" },
"addressLocality": { "type": "string" }
},
"required": ["@type", "streetAddress", "addressLocality"],
"additionalProperties": true
}
},
"required": ["@type", "name", "address"],
"additionalProperties": false
}
}
},
"required": ["@context", "@graph"],
"additionalProperties": true
}
When @embed is set to false:
{
"@type": "Article",
"title": {},
"author": {
"@embed": false,
"@type": "Person"
}
}
The algorithm produces a schema that accepts either a URI string or an object with just an @id (default behavior with graphOnly=false):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@context": {},
"@graph": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": { "const": "Article" },
"title": { "type": "string" },
"author": {
"oneOf": [
{ "type": "string", "format": "uri" },
{
"type": "object",
"properties": {
"@id": { "type": "string", "format": "uri" }
},
"required": ["@id"],
"additionalProperties": false
}
]
}
},
"required": ["@type", "title", "author"],
"additionalProperties": true
}
}
},
"required": ["@context", "@graph"],
"additionalProperties": true
}
Frame with array notation:
{
"@type": "Person",
"name": {},
"knows": [{
"@type": "Person",
"name": {}
}]
}
Generates an array schema (default behavior with graphOnly=false):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@context": {},
"@graph": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": { "const": "Person" },
"name": { "type": "string" },
"knows": {
"type": "array",
"items": {
"type": "object",
"properties": {
"@type": { "const": "Person" },
"name": { "type": "string" }
},
"required": ["@type", "name"],
"additionalProperties": true
}
}
},
"required": ["@type", "name", "knows"],
"additionalProperties": true
}
}
},
"required": ["@context", "@graph"],
"additionalProperties": true
}
Value objects allow properties to have both simple string values and structured objects with metadata:
{
"@context": {
"@vocab": "http://schema.org/"
},
"@type": "Article",
"headline": {
"@value": {},
"@language": "en"
}
}
The algorithm produces (with graphOnly=true):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@type": { "const": "Article" },
"headline": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"@value": {},
"@language": { "const": "en" }
},
"required": ["@value", "@language"],
"additionalProperties": false
}
]
}
},
"required": ["@type", "headline"],
"additionalProperties": true
}
This schema accepts both:
"headline": "Breaking News" (simple string)"headline": {"@value": "Breaking News", "@language": "en"} (language-tagged value)Language maps allow properties to specify values in multiple languages using @container: "@language":
{
"@context": {
"@vocab": "http://schema.org/",
"description": {
"@id": "http://schema.org/description",
"@container": "@language"
}
},
"@type": "Product",
"name": {},
"description": {}
}
The algorithm produces (with graphOnly=true):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@type": { "const": "Product" },
"name": { "type": "string" },
"description": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"patternProperties": {
"^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2}|-[0-9]{3})?(-[a-z0-9]+)*$": {
"type": "string"
}
},
"additionalProperties": false
}
]
}
},
"required": ["@type", "name", "description"],
"additionalProperties": true
}
This schema accepts both:
"description": "A great product" (simple string)"description": {"en": "A great product", "es": "Un gran producto", "es-419": "Un gran producto"} (language map)The BCP 47 regex pattern supports language codes like: en, en-US, es-419, zh-Hans-CN.
Different container types produce different schema structures:
{
"@context": {
"@vocab": "http://schema.org/",
"keywords": {
"@id": "http://schema.org/keywords",
"@container": "@set"
},
"metadata": {
"@id": "http://schema.org/metadata",
"@container": "@index"
}
},
"@type": "BlogPost",
"keywords": {},
"metadata": {}
}
The algorithm produces (with graphOnly=true):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"@type": { "const": "BlogPost" },
"keywords": {
"type": "array",
"uniqueItems": true
},
"metadata": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"required": ["@type", "keywords", "metadata"],
"additionalProperties": true
}
This schema validates:
keywords as an array with unique items (set semantics)metadata as an object with arbitrary string keys (index map)The following table summarizes the key mappings between JSON-LD Framing concepts and JSON Schema constructs:
| Frame Concept | JSON Schema Construct | Notes |
|---|---|---|
| Frame Object | type: "object" |
Basic structural mapping |
@type (single) |
const |
Exact type match |
@type (multiple) |
enum |
One of several types |
@id (required) |
type: "string", format: "uri" |
URI validation |
@embed: true |
Nested object schema | Default behavior |
@embed: false |
oneOf (URI or @id object) |
Reference-only |
@explicit: true |
additionalProperties: false |
Strict validation |
@explicit: false |
additionalProperties: true |
Default, allows extras |
@requireAll: true |
All properties in required |
All must be present |
@omitDefault: true |
Properties not in required |
All optional |
Array notation [{...}] |
type: "array", items: {...} |
Array of objects |
Empty object {} |
Required property with inferred type | Property must be present |
| Context type coercion | JSON Schema type inference | XSD types → JSON types |
@value with @language |
oneOf (string or value object) |
Language-tagged literals |
@value with @type |
oneOf (string or typed value object) |
Typed literals (e.g., dates) |
@container: "@language" |
oneOf (string or language map) |
Language map with BCP 47 pattern |
@container: "@index" |
type: "object" with additionalProperties |
Index map with arbitrary keys |
@container: "@set" |
type: "array", uniqueItems: true |
Set of unique items |
@container: "@list" |
type: "array" |
Ordered list |
Implementations SHOULD use existing JSON-LD libraries for context processing to ensure correct handling of context expansion, prefix resolution, and type coercion rules.
Implementations SHOULD provide clear error messages when:
Implementations SHOULD detect and handle circular references in nested frames to avoid infinite recursion. This can be accomplished by tracking the frame processing stack.
Implementations MAY validate generated schemas against the JSON Schema meta-schema to ensure correctness before returning them.
The following JSON-LD Framing features have limited or no mapping to JSON Schema:
When processing external contexts referenced by URL, implementations SHOULD implement appropriate security measures including:
Implementations SHOULD enforce limits on frame nesting depth to prevent stack overflow attacks from maliciously crafted frames.
This specification defines an algorithm for converting frame structures to schemas. The algorithm itself does not introduce privacy concerns beyond those inherent in JSON-LD and JSON Schema usage. Implementations should follow the privacy considerations outlined in the JSON-LD 1.1 specification.