Skip to content

JSON Wire Format

The JSON Wire Format specification defines how OJS job envelopes are serialized to and from JSON. JSON is the required baseline wire format: every conformant OJS implementation must be able to produce and consume jobs serialized as JSON.

This is Layer 2 of the OJS architecture, sitting between the Core Specification (what a job IS) and Protocol Bindings (how a job is TRANSMITTED).

The media type for an OJS job envelope encoded as JSON is:

application/openjobspec+json

Servers must accept application/json as a fallback alias. All JSON bodies must be UTF-8 encoded without a byte order mark, per RFC 8259.

OJS AttributeJSON KeyJSON TypeConstraints
Spec VersionspecversionstringMust be "1.0"
IDidstringUUIDv7 format
TypetypestringDot-namespaced, non-empty
QueuequeuestringLowercase [a-z0-9][a-z0-9\-\.]*
ArgumentsargsarrayJSON-native types only
OJS AttributeJSON KeyJSON TypeDescription
MetadatametaobjectString keys, JSON-native values
Priorityprioritynumber (integer)Higher values mean more important
Timeouttimeoutnumber (integer)Max execution time in seconds
Scheduled Atscheduled_atstringISO 8601 / RFC 3339 timestamp
Expires Atexpires_atstringISO 8601 / RFC 3339 timestamp
Retry PolicyretryobjectStructured retry configuration
Unique PolicyuniqueobjectDeduplication configuration
Visibility Timeoutvisibility_timeoutnumber (integer)Reservation period in seconds
OJS AttributeJSON KeyJSON TypeDescription
StatestatestringCurrent lifecycle state
Attemptattemptnumber (integer)Current attempt (1-indexed)
Created Atcreated_atstringWhen the job was created
Enqueued Atenqueued_atstringWhen the job entered the queue
Started Atstarted_atstringWhen a worker began execution
Completed Atcompleted_atstringWhen the job reached a terminal state
Resultresultany JSON typeJob return value
ErrorserrorsarrayError objects from failed attempts

The args array must contain only JSON-native types:

JSON TypeExample
string"hello"
number42, 3.14, -1
booleantrue, false
nullnull
array[1, "two", true]
object{"key": "value"}

Language-specific serialization formats (Python pickle, Ruby Marshal, Java serialization, PHP serialize, .NET BinaryFormatter) must never appear in args. This is the most critical security constraint in the specification.

OJS Core TypeJSON TypeNotes
StringstringMust be valid UTF-8
IntegernumberNo fractional part. Safe up to 2^53 - 1
FloatnumberIEEE 754 double-precision
Booleanboolean
NullnullExplicit absence
TimestampstringISO 8601 / RFC 3339
DurationstringISO 8601 (e.g., "PT30S")
IdentifierstringUUIDv7, lowercase hex with hyphens
Binarystringbase64url-encoded

Integers larger than 2^53 - 1 must be encoded as strings to prevent precision loss in JavaScript.

All timestamps must conform to RFC 3339:

2025-06-01T09:00:00Z -- UTC, no fractional seconds
2025-06-01T09:00:00.000Z -- UTC, millisecond precision
2025-06-01T09:00:00.123456Z -- UTC, microsecond precision
2025-06-01T11:00:00+02:00 -- Explicit timezone offset

Timestamps without timezone information (e.g., "2025-06-01T09:00:00") must be rejected. UTC with the Z suffix is recommended.

Duration fields use ISO 8601 format: "PT1S" (1 second), "PT5M" (5 minutes), "PT1H" (1 hour), "P1D" (1 day).

Job IDs must be UUIDv7 values, serialized as lowercase hyphenated strings:

019539a4-b68c-7def-8000-1a2b3c4d5e6f

UUIDv7 is time-ordered and sortable, enabling efficient database indexing and chronological job listing without coordination.

Binary data within args, meta, or result must use base64url encoding (RFC 4648 Section 5) without padding characters. Producers should include a sibling field like content_encoding: "base64url" to signal that a string value contains binary data.

For large binary payloads, use external references (URIs, S3 paths) rather than inline encoding. Base64url inflates data size by approximately 33%.

Implementations must support envelopes up to 1 MiB (1,048,576 bytes) in their JSON-serialized form.

Recommended field-level limits:

FieldMaximumRationale
type255 charactersEnough for deep dot-namespacing
queue255 charactersEnough for hierarchical queue names
args64 KiBArguments should be references, not large blobs
meta16 KiBMetadata should be concise
args depth10 levelsDeeply nested structures indicate design issues

Batch requests must support at least 1,000 jobs per request.

Multiple jobs can be submitted in a single request:

{
"jobs": [
{
"specversion": "1.0",
"id": "019539a4-b68c-7def-8000-1a2b3c4d5e6f",
"type": "email.send",
"queue": "email",
"args": ["alice@example.com", "welcome"]
},
{
"specversion": "1.0",
"id": "019539a4-b68c-7def-8000-2b3c4d5e6f7a",
"type": "email.send",
"queue": "email",
"args": ["bob@example.com", "welcome"]
}
]
}

Each job in the array must be independently valid. Batch processing should be atomic where the backend supports it.

Errors in JSON responses use the following structure:

{
"error": {
"code": "invalid_request",
"message": "Job envelope validation failed: 'type' is required",
"retryable": false,
"details": {
"validation_errors": [
{ "path": "$.type", "message": "required field missing" }
]
},
"request_id": "req_019539a4-c000-7def-8000-aabbccddeeff"
}
}
CodeDescriptionRetryable
invalid_requestMalformed JSON or missing required fieldsNo
invalid_payloadJob envelope fails schema validationNo
schema_validationArgs do not conform to registered schemaNo
not_foundJob or resource does not existNo
duplicateUnique constraint violatedNo
queue_pausedTarget queue is pausedYes
rate_limitedRate limit exceededYes
backend_errorBackend storage/transport failureYes
timeoutOperation timed outYes
unsupportedFeature not supported at this levelNo
envelope_too_largeJob envelope exceeds size limitNo
  • Field ordering: Implementations must not depend on the order of fields in a JSON object.
  • Null handling: Absent optional fields should be omitted rather than included with null.
  • Unknown fields: Must be preserved during round-tripping for forward compatibility.
  • Whitespace: Accept both compact and pretty-printed. Produce compact encoding for transmission.
  • Duplicate keys: Producers must not emit duplicate keys. Consumers should use the last value encountered.