OSON Specification

Version 1.0

OSON, Oracle binary JSON, is a self-contained binary tree encoding for JSON documents. An OSON value stores a root header, a field-name dictionary, and a tree-node segment so that readers can navigate directly to object members, array elements, and scalar values without reparsing JSON text.

This document describes the public wire format at the level needed to identify, validate, traverse, and interoperate with OSON payloads. It follows the compact style of the BSON specification while preserving OSON terminology.

This public subset documents ordinary instance OSON images and the decoder rules needed to read partially updated images for versions 0x01, 0x02, 0x03, and 0x04. The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY, and OPTIONAL are to be interpreted as described by RFC 2119 and RFC 8174.

OSON is a binary representation for the SQL/JSON native JSON datatype defined by ISO/IEC 9075-2:2023. In addition to JSON object, array, string, number, boolean, and null values, OSON can carry SQL scalar payloads such as DATE, TIMESTAMP, TIMESTAMP WITH TIME ZONE, BINARY_FLOAT, BINARY_DOUBLE, RAW or BINARY, INTERVAL YEAR TO MONTH, and INTERVAL DAY TO SECOND. The OSON framing described here is sufficient to navigate, copy, and round-trip documents; implementations that need bit-exact interpretation of Oracle-native scalar payload bytes should reuse the corresponding Oracle datatype codecs.

Basic Types

TypeMeaning
byteUnsigned 8-bit value.
uint16Unsigned 16-bit integer, big-endian.
uint32Unsigned 32-bit integer, big-endian.
uint64Unsigned 64-bit integer, big-endian.
int8, int16, int32, int64Signed two's-complement integer of the given width, big-endian for multibyte widths.
bytes(n)Exactly n bytes.
cstringUTF-8 bytes terminated by 0x00; the terminator is not part of the value.
utf8(n)n bytes containing UTF-8 text.
offset32uint32 byte offset from the beginning of the OSON payload unless a version or flag explicitly marks relative-offset mode.

All multibyte integer fields in OSON are encoded in big-endian order for platform-independent interchange.

Top-Level Format

oson-document ::= magic version body
magic         ::= 0xff 0x4a 0x5a        // 0xff 'J' 'Z'
version       ::= 0x01 | 0x02 | 0x03 | 0x04
body          ::= header-fields field-name-dictionary tree-node-segment
VersionMeaning
0x01Original instance format with one-byte field-name length entries.
0x02Version 1 plus an extended tree segment used by partial update and growth handling.
0x03Version 1 plus two-byte field-name length entries.
0x04Version 3 plus the extended tree segment.

Logical Layout

oson-instance ::= root-header field-name-dictionary tree-node-segment
field-name-dictionary ::= sorted-field-name-hash-array field-name-offset-array field-name-bytes
tree-node-segment ::= root-node node-record* shared-node-segment? overflow-or-update-segment?

The field-name dictionary assigns a field id (FID) to each distinct object field name. Object nodes store FIDs rather than repeated field names.

Design Rationale

GoalFormat support
Jump navigationContainer nodes store child offsets, allowing a decoder to jump directly to a requested object field or array element after resolving the field id or element index.
Dictionary compressionDistinct field names are stored once in the field-name dictionary and are referenced by small FID values in object nodes.
Decoder-compatible partial updateVersions 0x02 and 0x04 keep the ordinary node model and add forwarding records so a decoder can resolve updated nodes from an appended extended tree segment.
Compact storageShared simple nodes, shared scalar nodes, relative offsets, and FID-array sharing reduce space without changing the logical JSON value exposed by a decoder.

Detailed Segment Layout

2.1 Root Header Structure

The root header starts every ordinary self-contained OSON instance. The first six bytes are always present:

root-header-prefix ::= 0xff 0x4a 0x5a version flag1 flag2
ByteFieldMeaning
00xffJZNOCT_MAGIC.
10x4aASCII J, JZNOCT_MAGICJ.
20x5aASCII Z, JZNOCT_MAGICZ.
3version0x01, 0x02, 0x03, or 0x04.
4flag1First persistent root-header flag byte.
5flag2Second persistent root-header flag byte.

Version 0x01 is the base instance format. Version 0x02 means the instance may contain partial-update extension structures. Version 0x03 means the instance contains a secondary dictionary for field names whose byte length is greater than 255 and no greater than 65535. Version 0x04 combines the version 3 dictionary extension with the version 2 partial-update extension structures.

flag1 bit values:

MaskNameMeaning
0x01JZNOCT_HID_USEUB1Primary dictionary hash-id entries use the most significant one byte of the four-byte field-name hash. This bit is expected when primary dictionary entries exist.
0x02JZNOCT_HID_USEUB2 (reserved)Must be zero. The historical source name indicates a two-byte primary hash-id experiment, but compliant persistent OSON does not set this bit. Decoders MUST reject an instance with this bit set.
0x04JZNOCT_TOT_DISFNM_UB2The count of distinct primary field names is stored as uint16; otherwise the count is one byte unless flag2 0x08 selects uint32. Object FID entries for the primary dictionary are also uint16 when this bit is set.
0x08JZNOCT_FLDNM_SZ_UB4The primary field-name heap size is stored as uint32; otherwise it is stored as uint16.
0x10JZNOCT_TREE_SZ_UB4The tree-node segment size is stored as uint32; otherwise it is stored as uint16.
0x20JZNOCT_TINY_NODE_STATTiny-node statistics are present. A tiny node is an object or array node whose storage is smaller than five bytes, or an object node whose FID array is referenced by another object. Scalar nodes are not counted as tiny nodes.
0x40JZNOCT_HAS_SHR_NODES_SEG_CLUSTERA clustered shared-node segment is present inside the tree-node segment. In this form the root header's tot-tiny-node field stores the shared-node segment offset; the actual tiny-node count, when present, is stored in the shared-node segment.
0x80JZNOCT_FID_NO_SORTObject FID arrays in this instance are not globally guaranteed to be sorted by FID. Individual object opcodes can also carry the unsorted-object bit.

flag2 bit values:

MaskNameMeaning
0x01JZNOCT2_REL_OFFSETContainer child offsets are stored relative to the containing object or array node address. A decoder reads the stored uint16 or uint32, then adds it to the parent node offset with the same-width unsigned arithmetic.
0x02JZNOCT2_INLINE_LEAFScalar leaf bytes are stored inline in the scalar node. Current encoders set this bit.
0x04JZNOCT2_SLEN_IN_PCODECommon scalar lengths are encoded in the scalar opcode. Current encoders set this bit together with 0x02.
0x08JZNOCT2_TOT_DISFNM_UB4The count of distinct primary field names is stored as uint32. Object FID entries for the primary dictionary are uint32. This overrides flag1 0x04.
0x10JZNOCT2_J_SCALARThe top-level JSON value is a scalar. The tree-node segment contains a single scalar node.
0x20JZNOCT2_SHR_SIMP_NODESSharable simple nodes such as true, false, null, empty string, empty object, empty array, zero, and one may be de-duplicated.
0x40JZNOCT2_SHR_NODESSharable scalar nodes may be de-duplicated in addition to simple nodes.
0x80reservedMust be zero. Decoders MUST reject an instance with this bit set.

The header fields after flag2 are width-selected by these flags:

root-header ::= root-header-prefix
                primary-field-name-count
                primary-field-name-heap-size
                v3v4-extra-header?
                tree-node-segment-size
                tot-tiny-node
FieldWidthMeaning
primary-field-name-count1 byte, uint16, or uint32Number of distinct field names whose UTF-8 byte length is at most 255. Width is one byte by default, uint16 when flag1 & 0x04 is set, and uint32 when flag2 & 0x08 is set.
primary-field-name-heap-sizeuint16 or uint32Size in bytes of the primary field-name heap. Width is selected by flag1 & 0x08.
v3v4-extra-header10 bytesPresent only for version 0x03 or 0x04.
tree-node-segment-sizeuint16 or uint32Size in bytes of the tree-node segment. Width is selected by flag1 & 0x10.
tot-tiny-nodeuint16Tiny-node count when flag1 & 0x20 is set and no clustered shared-node segment redirects the field. If flag1 & 0x40 is set, this field stores the tree-node-segment offset of the shared-node segment.

Version 0x03 and 0x04 add the following 10 bytes before tree-node-segment-size:

FieldWidthMeaning
pflag31 byteAdditional persistent flag byte for the secondary dictionary. 0x01 (JZNOCT3_FLDNM2_SZ_UB2) means secondary field-name offsets are uint16; otherwise they are uint32. Bits 0x02 through 0x80 are reserved and must be zero.
pflag41 byteReserved. Must be zero.
secondary-field-name-countuint32Number of distinct field names whose UTF-8 byte length is greater than 255 and no greater than 65535.
secondary-field-name-heap-sizeuint32Size in bytes of the secondary field-name heap.

2.2 Field Name Dictionary

The field-name dictionary is split into a primary dictionary and, for version 0x03 or 0x04, an optional secondary dictionary. Each dictionary has three arrays in this order:

field-name-dictionary ::= primary-hash-id-array
                          primary-field-name-offset-array
                          primary-field-name-heap
                          secondary-hash-id-array?
                          secondary-field-name-offset-array?
                          secondary-field-name-heap?

Primary dictionary entries represent field names whose UTF-8 byte length is at most 255. The primary hash-id array contains one hash-id entry per distinct primary field name. Each hash-id entry is the most significant byte of the fixed four-byte field-name hash. The primary field-name offset array contains one offset per entry; each offset is relative to the start of the primary field-name heap and is uint16 unless flag1 & 0x08 selects uint32. Each primary heap record is:

primary-field-name-record ::= uint8-byte-length utf8(byte-length)

The FID for a primary field name is its zero-based array index in the sorted primary hash-id array. Object nodes store FIDs using one byte by default, uint16 when flag1 & 0x04 is set, and uint32 when flag2 & 0x08 is set.

Secondary dictionary entries represent field names whose UTF-8 byte length is greater than 255 and no greater than 65535. They exist only in version 0x03 or 0x04. Each secondary hash-id entry is the most significant two bytes of the same fixed four-byte field-name hash. Secondary field-name offsets are uint16 when pflag3 & 0x01 is set and uint32 otherwise. Each secondary heap record is:

secondary-field-name-record ::= uint16-byte-length utf8(byte-length)

Secondary FIDs follow all primary FIDs: secondary-fid = primary-field-name-count + secondary-index. A decoder can determine which dictionary to use by comparing the FID with primary-field-name-count.

Hashing and sorting are part of the persistent format. The field-name hash is FNV-1a over the UTF-8 bytes, starting with 0x811c9dc5, multiplying by 16777619 after each XOR, and then treating the result as big-endian for the stored most-significant-byte comparison. Primary entries sort by the most significant hash byte, then one-byte field-name length, then UTF-8 byte comparison. Secondary entries sort by the most significant two hash bytes, then two-byte field-name length, then UTF-8 byte comparison. Hash collisions are resolved by comparing length and then the actual UTF-8 field-name bytes.

2.3 Shared Node Segment

When flag1 & 0x40 (JZNOCT_HAS_SHR_NODES_SEG_CLUSTER) is set, shared nodes are clustered in a shared-node segment embedded in the tree-node segment. The root header's tot-tiny-node field gives the tree-node-segment offset of this segment. The segment is normally placed immediately after the root node, but decoders MUST use the header offset rather than infer the location from the root-node size.

shared-node-segment ::= shared-node-segment-opcode
                        tiny-node-count?
                        shared-simple-node-region
                        shared-scalar-region-length?
                        shared-scalar-node-region?

The one-byte shared-node-segment-opcode is a bit field:

MaskNameMeaning
0x80JZN0_SHARE_SEG_HAS_VARLENA shared-scalar-node region is present, and a region length follows the simple-node region.
0x40JZN0_SHARE_SEG_VARLEN_UB4The shared-scalar-node region length is uint32; otherwise it is uint16. This bit is meaningful only when 0x80 is set.
0x3fsimple-region lengthLength in bytes of the shared-simple-node region. The maximum directly encoded simple-region length is 63 bytes.

If flag1 & 0x20 (JZNOCT_TINY_NODE_STAT) is set, tiny-node-count is a uint16 stored immediately after the opcode. If that root flag is clear, no tiny-node count is stored and the count is zero. The shared-simple-node region contains complete OSON node records for simple shared values. The shared-scalar-node region, when present, contains complete scalar node records for de-duplicated scalar values. Child offsets that resolve inside this segment are interpreted exactly like offsets to ordinary tree nodes; the segment changes ownership and update rules, not the node opcode grammar.

2.4 Tree-Node Segment

The tree-node segment is a byte serialization of object, array, and scalar nodes. Every node has a tree-node-segment offset relative to the beginning of this segment. Container child-offset arrays store these offsets as uint16 or uint32; when root flag2 & 0x01 is set, the stored value is relative to the containing node and must be translated to an absolute tree-node-segment offset before dereferencing.

BitsMasked valueNode class
00xxxxxx0x00Scalar opcode family.
01xxxxxx0x40Scalar opcode family or partial-update marker.
10xxxxxx0x80Object node.
11xxxxxx0xc0Array node.

Container opcode bits:

MaskNameApplies toMeaning
0xc0JZNOCT_TYP_BITMASKobject/array0x80 (JZNOCT_OBJECT_TYP) means object; 0xc0 (JZNOCT_ARRAY_TYP) means array.
0x20JZNOCT_OFFSET_SIZE_BITobject/arrayChild offsets are uint32 when set and uint16 when clear.
0x18JZNOCT_CHILDREN_SIZE_BITobject/arraySelects the child-count width: 0x00 one byte, 0x08 uint16, 0x10 uint32. For objects only, 0x18 means the FID array is referenced from a primary object instead of stored inline.
0x04JZNOCT_OBJ_FID_UNSORTED_BITobjectThis object's FID array is not sorted by FID.
0x02JZNOCT_OBJ_FID_REFERREDobjectThis object owns a FID array referenced by other object nodes with the same object definition.
0x01JZNOCT_UPD_OBJ_OVFLWobjectThis referred object was updated through overflow-address indirection. This bit is valid only with 0x02.
object-node ::= object-opcode child-count fid-array child-offset-array
object-fid-reference-node ::= object-opcode primary-object-offset child-offset-array
array-node ::= array-opcode child-count child-offset-array

Normal objects store a FID array and a child-offset array with matching cardinality. A FID-array reference object is selected when (opcode & 0x18) == 0x18; its referenced primary object MUST be an object with JZNOCT_OBJ_FID_REFERRED set. Arrays use the same child-count and child-offset width bits as objects, but 0x18 is reserved for object FID-array references and MUST NOT be used as an array child-count width.

Scalar opcodes use the low six bits as scalar type and, for compact families, inline length. The scalar opcode reference below gives the payload for each scalar opcode. The partial-update marker opcodes are 0x75 (JZNOCT_UPD_OVFLW), 0x76 (JZNOCT_UPD_UB2_FWA), 0x77 (JZNOCT_UPD_UB4_FWA), and 0x78 (JZNOCT_UPD_XSZ_RES). A decoder MUST resolve these markers to the replacement node before exposing the logical value.

Conceptual Grammar

OSON_FORMAT := FIELD_NAME_DICTIONARY ROOT_NODE_OFFSET NODE_SEGMENT
FIELD_NAME_DICTIONARY := SORTED_FIELD_NAME_HASH_CODE_ARRAY FIELD_NAME_OFFSET_ARRAY FIELD_NAME_HEAP
FID(field-name) := index of field-name in SORTED_FIELD_NAME_HASH_CODE_ARRAY
JUMP_TO(ROOT_NODE_OFFSET) := OBJECT_NODE | ARRAY_NODE | SCALAR_NODE | FORWARD_OFFSET
OBJECT_NODE := OBJECT_OPCODE OBJECT_CONTENT
OBJECT_CONTENT := SORTED_FID_ARRAY CHILD_NODE_OFFSET_ARRAY | SORTED_FID_ARRAY_OFFSET CHILD_NODE_OFFSET_ARRAY
ARRAY_NODE := ARRAY_OPCODE CHILD_NODE_OFFSET_ARRAY
SCALAR_NODE := SCALAR_OPCODE SCALAR_LENGTH? SCALAR_BYTES?
JUMP_TO(FORWARD_OFFSET) := NODE_OFFSET

Field-Name Dictionary

ComponentDescription
Sorted hash arrayHash values for field names, sorted to make lookup efficient.
Field-name offset arrayOffsets from the dictionary base to each field-name byte string.
Field-name heapUTF-8 field names. Version 1 and 2 entries use one-byte name lengths; version 3 and 4 entries use two-byte name lengths.

Node Kinds

Node kindContent
ObjectA count, sorted FID array or FID-array reference, and child node offsets.
ArrayA count and child node offsets in array order.
ScalarA scalar opcode, optional length, and scalar bytes. Small scalars can be encoded entirely in the opcode.
Forwarding offsetOffset to a replacement node, used by partial update.

Opcode Reference

This section lists the persistent node opcodes and opcode patterns used by OSON instance documents. All integer counts, offsets, lengths, and fixed-width scalar values following an opcode are stored in big-endian order unless otherwise stated.

Opcode bitsHex rangeNode classDecoder action
00xxxxxx0x00-0x3fScalar or scalar extensionInterpret using the scalar opcode table.
01xxxxxx0x40-0x7fScalar, scalar extension, or partial-update markerInterpret using the scalar and update opcode tables.
10xxxxxx0x80-0xbfObject nodeInterpret low bits as object flags, count size, and offset size.
11xxxxxx0xc0-0xffArray nodeInterpret low bits as array flags, count size, and offset size.

Object and Array Node Opcodes

Container opcodes are bit fields rather than one fixed byte per shape.

Bit maskNameApplies toMeaning
0xc0node typeobject/array0x80 means object; 0xc0 means array.
0x20offset widthobject/arrayOff: child offsets are uint16. On: child offsets are uint32.
0x18child-count widthobject/array0x00: one-byte count; 0x08: uint16; 0x10: uint32; 0x18: object-only FID-array reference.
0x04object FID unsortedobjectObject field-id array is not sorted by FID.
0x02object FID referredobjectThis object owns a field-id array referenced by other object nodes with the same object definition.
0x01object update overflowobjectUsed with 0x02 during partial update when a referenced object definition is updated through overflow indirection.
object-normal ::= opcode child-count field-id-array child-offset-array
object-fid-ref ::= opcode primary-object-offset child-offset-array
array ::= opcode child-count child-offset-array
OpcodeMeaning
0x80Object with one-byte child count and uint16 child offsets.
0x84Object with one-byte child count, uint16 child offsets, and the object FID-unsorted flag set; commonly used for the compact empty-object image 0x84 0x00.
0x88Object with uint16 child count and uint16 child offsets.
0x90Object with uint32 child count and uint16 child offsets.
0xa0Object with one-byte child count and uint32 child offsets.
0xa8Object with uint16 child count and uint32 child offsets.
0xb0Object with uint32 child count and uint32 child offsets.
0x98Object FID-array reference with uint16 primary-object offset.
0xb8Object FID-array reference with uint32 primary-object offset.
0xc0Array with one-byte child count and uint16 child offsets; the compact empty-array image is 0xc0 0x00.
0xc8Array with uint16 child count and uint16 child offsets.
0xd0Array with uint32 child count and uint16 child offsets.
0xe0Array with one-byte child count and uint32 child offsets.
0xe8Array with uint16 child count and uint32 child offsets.
0xf0Array with uint32 child count and uint32 child offsets.

Scalar Node Opcodes

Opcode or rangeNamePayload after opcodeDecoded valueNotes
0x00-0x1fshort UTF-8 stringn UTF-8 bytes, where n = opcode & 0x1fJSON stringCovers lengths 0 through 31.
0x20-0x2fshort Oracle NUMBERn Oracle NUMBER bytes, where n = (opcode & 0x0f) + 1JSON numberCovers lengths 1 through 16.
0x30nullnoneJSON nullNo payload.
0x31truenoneJSON boolean trueNo payload.
0x32falsenoneJSON boolean falseNo payload.
0x33UTF-8 string with one-byte lengthuint8 length, then UTF-8 bytesJSON stringFor lengths less than 256.
0x34Oracle NUMBER with one-byte lengthuint8 length, then Oracle NUMBER bytesJSON numberLength must be nonzero.
0x35numeric textuint8 length, then numeric text bytesJSON numberInternal/test number text path.
0x36binary double8 bytesSQL/JSON binary doubleOracle/canonical binary double form.
0x37UTF-8 string with uint16 lengthuint16 length, then UTF-8 bytesJSON stringLong string.
0x38UTF-8 string with uint32 lengthuint32 length, then UTF-8 bytesJSON stringVery large string.
0x39timestamp11 bytesSQL timestampOracle timestamp with fractional seconds.
0x3abinary with uint16 lengthuint16 length, then bytesBinary/rawFor binary payloads smaller than 64 KiB.
0x3bbinary with uint32 lengthuint32 length, then bytesBinary/rawFor larger binary payloads.
0x3cdate7 bytesSQL dateOracle date binary form.
0x3dyear-month interval5 bytesSQL interval year to monthYears use bias +2147483648; months use bias +60.
0x3eday-second interval11 bytesSQL interval day to secondOracle interval day-second binary form.
0x3fother/reserved extensionimplementation-definedimplementation-definedReject unless explicitly supported.
0x40-0x47signed 32-bit integer as Oracle NUMBERn = opcode & 0x07 Oracle NUMBER bytesNumberValid lengths 1 through 7.
0x48-0x4freservednonenoneReject unless assigned by a future version.
0x50-0x5fsigned 64-bit integer as Oracle NUMBERn = opcode & 0x0f Oracle NUMBER bytesNumberValid lengths 1 through 12.
0x60-0x6fDecimal128 as Oracle NUMBERn = (opcode & 0x0f) + 1 Oracle NUMBER bytesDecimal128-preserving numberCovers lengths 1 through 16.
0x70-0x73reservednonenoneReject unless assigned by a future version.
0x74Decimal128 Oracle NUMBER with one-byte lengthuint8 length, then Oracle NUMBER bytesDecimal128-preserving numberFor payload length greater than 16.
0x75update overflow markernone in nodeforwarding nodeForwarding address is in the overflow-address map.
0x76update uint16 forwarding addressuint16 forwarding-addressforwarding nodePartial-update indirection.
0x77update uint32 forwarding addressuint32 forwarding-addressforwarding nodePartial-update indirection.
0x78update reserved-growth wrapperone growth-size byte, then another nodewrapped nodeExtra room for in-place growth.
0x79native integersubtype byte, then integer bytesInteger numberSee native-integer subtype table.
0x7areservednonenoneReject unless assigned by a future version.
0x7bextended binary typesubtype byte, uint32 length, then payloadExtended scalarSee extended-binary subtype table.
0x7ctimestamp with time zone13 bytesSQL timestamp with time zone11 timestamp bytes plus 2 time-zone bytes.
0x7dtimestamp77 bytesSQL timestampZero fractional seconds.
0x7egeneric IDuint8 length, then ID bytesID valueUsed for ObjectId, UUID, ROWID, and similar IDs.
0x7fbinary float4 bytesSQL/JSON binary floatOracle/canonical binary float form.

Native Integer Subtypes

SubtypePayloadMeaning
0x011 byteUnsigned 8-bit integer.
0x811 byteSigned 8-bit integer.
0x022 bytesUnsigned 16-bit integer, big-endian.
0x822 bytesSigned 16-bit integer, two's-complement big-endian.
0x044 bytesUnsigned 32-bit integer, big-endian.
0x844 bytesSigned 32-bit integer, two's-complement big-endian.
0x088 bytesUnsigned 64-bit integer, big-endian.
0x888 bytesSigned 64-bit integer, two's-complement big-endian.

Extended Binary Subtypes

SubtypeMeaningPayload
0x01VectorVector embedding binary image.
0x02Embedded OSONReserved for OSON embedded in OSON. Portable encoders should avoid it unless both endpoints define the payload contract.
0x03Scalar arrayPacked homogeneous scalar-array image.

Decoder Validation for Opcodes

  1. Reject scalar opcodes whose length fields exceed the containing tree segment.
  2. Reject Oracle NUMBER opcode families with zero payload length where the family requires a nonzero length.
  3. Reject object or array opcodes with child-count width 0x18 unless the node is an object FID-array reference.
  4. Reject array opcodes with object-only flags set.
  5. Reject object FID-array references whose primary-object offset does not resolve to an object with the referred bit set.
  6. Reject partial-update forwarding chains that point outside the original or extended tree segment, or that cycle.
  7. Reject reserved opcodes or subtypes unless explicitly supported.

Scalar Types

FamilyPayload
NullNo payload.
Boolean true/falseNo payload.
UTF-8 stringLength plus UTF-8 bytes. Compact string opcodes can encode short lengths in the opcode.
JSON numberOracle NUMBER bytes, decimal text/number form, IEEE float/double, Decimal64/Decimal128, or native integer form depending on encoder choice and type preservation flags.
Native integerint8, unsigned byte, int16, uint16, int32, uint32, int64, or uint64 in two's-complement big-endian form. The native-integer extension opcode is 0x79.
BinaryLength plus uninterpreted bytes.
IDFixed-length binary identifiers such as BSON ObjectId, UUID, or Oracle ROWID. UUID is 16 bytes.
DateOracle date bytes.
TimestampOracle timestamp bytes, either 7-byte timestamp-with-zero-fraction form or 11-byte full timestamp form.
Timestamp with time zoneOracle timestamp-with-time-zone bytes. Public opcode family includes 0x7c.
Year-month intervalOracle year-month interval bytes. Public opcode family includes 0x3d.
Day-second intervalOracle day-second interval bytes. Public opcode family includes 0x3e.
Binary floatIEEE 754 binary32 or Oracle binary-float bytes. Public opcode family includes 0x7f.
Binary doubleIEEE 754 binary64 or Oracle binary-double bytes. Public opcode family includes 0x36.
VectorExtended binary type for vector embeddings. The vector payload uses the extended binary type container.
Scalar arrayExtended binary type for homogeneous SQL scalar arrays.

Compact Encoding and Shared Nodes

ModeDescriptionPartial-update implication
No compact sharingEach logical node has its own physical node.A scalar can be directly replaced when the new encoded value fits in the old node.
Shared simple nodesCommon simple values such as true, false, null, empty string, empty object, and empty array can be shared.Shared simple nodes cannot be directly overwritten for one occurrence.
Shared scalar nodesRepeated scalar values can be shared.A shared scalar cannot be directly overwritten for one occurrence.
Clustered shared segmentShared nodes are stored together in a shared-node segment embedded in the tree-node segment.Readers treat child offsets into the shared segment as normal node references; updaters must preserve sharing semantics.

Section 3. OSON Segments for Partial Update

OSON supports direct in-place replacement when an updated node fits in the original node storage. Direct replacement does not change the OSON version and does not append any segment. When the updated content does not fit, OSON appends update structures after the original tree-node segment and changes the instance version from 0x01 to 0x02, or from 0x03 to 0x04 when the instance uses the secondary field-name dictionary.

version-1-or-3-layout ::= root-header field-name-dictionary tree-node-segment
version-2-or-4-layout ::= root-header field-name-dictionary tree-node-segment
                          update-header overflow-address-map-segment extended-tree-segment

The overflow-address-map segment can have size zero when the original document has no tiny nodes. A tiny node is a node whose original storage is too small to hold a forwarding opcode plus a forwarding address. New content produced by partial update is always written into the extended tree segment. Original nodes that no longer hold their current content are rewritten as forwarding markers.

3.1 Update Header

The update header is fixed-size and immediately follows the original tree-node segment. JZNOCT_UPD_HEADER_SZ is 16 bytes.

update-header ::= update-flag1 update-flag2 overflow-entry-count
                  reserved4 overflow-map-size extended-tree-size
OffsetFieldWidthMeaning
0update-flag11 byteFirst persistent partial-update flag byte.
1update-flag21 byteReserved. Must be zero. Decoders MUST reject a nonzero value.
2overflow-entry-countuint16Number of used tiny-node address mappings in the overflow-address-map segment. Maximum is JZNOCT_UPD_OVFLW_MAX_ENTRY, 1024.
4reserved44 bytesReserved. Must be all zero. Decoders MUST reject nonzero bytes.
8overflow-map-sizeuint32Allocated size in bytes of the overflow-address-map segment. This size is chosen when the update segments are first appended and remains fixed for the instance. Maximum is JZNOCT_UPD_MAX_OVFLW_ADDR_SEG_SZ, 8192.
12extended-tree-sizeuint32Allocated or used size in bytes of the extended tree segment so far. It grows as update appends add new node images.
update-flag1 maskNameMeaning
0x01JZNOCTUPDHDR_OVFLW_SEG_UB2Overflow-address-map entries are (uint16 original-node-offset, uint16 forwarding-offset), 4 bytes per entry.
0x02 through 0x80reservedMust be zero. Decoders MUST reject an instance with any of these bits set.

update-flag2 is reserved as an entire byte. It MUST be zero.

3.2 Overflow-Address-Map Segment

update-flag1 & 0x01Entry layoutEntry size
Setuint16 original-node-offset, uint16 forwarding-offset4 bytes
Clearuint32 original-node-offset, uint32 forwarding-offset8 bytes

The first value in each entry is the tree-node-segment offset of the original tiny node. The second value is the forwarding offset for the replacement node in the extended tree segment. All values are big-endian. Decoders should load this segment into an address map before resolving JZNOCT_UPD_OVFLW or object-reference overflow markers. Encoders choose the 4-byte entry form when the original tree plus allowed growth fits in the uint16 address space; otherwise they use the 8-byte form.

3.3 Extended Tree Segment

The extended tree segment stores complete OSON node images appended by partial update. A forwarding offset is relative to the beginning of the extended tree segment address space. To resolve it, add the forwarding offset to:

extended-tree-base = original-tree-node-segment-size
                     + JZNOCT_UPD_HEADER_SZ
                     + overflow-map-size
max-growth = (original-tree-node-segment-size
              + JZNOCT_UPD_HEADER_SZ
              + JZNOCT_UPD_MAX_OVFLW_ADDR_SEG_SZ) * 0.25

If partial update would exceed the overflow map limit or the extended tree growth limit, the writer should replace the full OSON document with a newly encoded compact instance rather than append more update records.

3.4 Partial-Update Forwarding Opcodes and Bits

Opcode or maskNameMeaning
0x75 (01110101)JZNOCT_UPD_OVFLWOriginal node is a tiny node. Its replacement forwarding offset is stored in the overflow-address-map segment under this original node offset.
0x76 (01110110)JZNOCT_UPD_UB2_FWAThe next two bytes are a big-endian uint16 forwarding offset into the extended tree segment. The marker occupies 3 bytes total.
0x77 (01110111)JZNOCT_UPD_UB4_FWAThe next four bytes are a big-endian uint32 forwarding offset into the extended tree segment. The marker occupies 5 bytes total.
0x78 (01111000)JZNOCT_UPD_XSZ_RESReserved-growth wrapper. The next byte records reserved growth size; the real node begins after the two-byte wrapper.
mask 0x83 (10000011)JZNOCT_UPD_OBJ_REF_BITMASKObject-reference overflow marker. ((opcode & 0x83) == 0x83) means an object whose FID array is referred by other objects has been updated through overflow indirection.

For JZNOCT_UPD_OVFLW and the object-reference overflow marker, the original node offset is looked up in the overflow-address-map segment. For JZNOCT_UPD_UB2_FWA and JZNOCT_UPD_UB4_FWA, the forwarding offset is stored inline after the marker opcode. Readers MUST reject cycles and out-of-range forwarding targets.

3.5 Partial-Update Writer Rules

  1. If replacement content fits in the old node storage, rewrite the node in place and leave the version unchanged.
  2. If replacement content does not fit but the old node has room for 0x76 plus two bytes, write JZNOCT_UPD_UB2_FWA and a uint16 forwarding offset.
  3. If replacement content does not fit but the old node has room for 0x77 plus four bytes, write JZNOCT_UPD_UB4_FWA and a uint32 forwarding offset.
  4. If the old node is too small for an inline forwarding address, write JZNOCT_UPD_OVFLW or the object-reference overflow bit pattern and add an overflow-address-map entry.
  5. If any size limit is exceeded, perform a full document replacement.

3.6 Partial-Update Reader Algorithm

A decoder reads a version 0x02 or 0x04 image as an ordinary OSON tree until it reaches a forwarding marker. It then resolves the marker to a complete replacement node in the extended tree segment and decodes that replacement node using the same object, array, and scalar rules.

parse-update-extension:
  update-start = tree-start + original-tree-node-segment-size
  read update-flag1, update-flag2, overflow-entry-count
  require update-flag2 == 0
  require overflow-entry-count <= 1024
  require reserved4 == 0
  read overflow-map-size and extended-tree-size
  require overflow-map-size <= 8192
  map-start = update-start + 16
  extended-tree-start = map-start + overflow-map-size
  require extended-tree-start + extended-tree-size <= payload-size
  read overflow-entry-count pairs into overflow-map

resolve-forwarding-marker(original-offset, opcode):
  if opcode == 0x75:
    forward = overflow-map[original-offset]
  else if opcode == 0x76:
    forward = uint16 at original-offset + 1
  else if opcode == 0x77:
    forward = uint32 at original-offset + 1
  else if ((opcode & 0x83) == 0x83):
    forward = overflow-map[original-offset]
  else if opcode == 0x78:
    reject reserved growth wrapper
  require forward exists and forward < extended-tree-size
  decode node at extended-tree-start + forward

A decoder MUST bounds-check every forwarding address before dereferencing it. A decoder SHOULD reject forwarding cycles or excessive forwarding depth. Valid images resolve forwarding markers to ordinary node content without requiring unbounded chains.

Decoder Algorithm

The following pseudocode summarizes the behavior of a conformant decoder. It is not a required API shape, but the checks and dispatch order are normative.

Parse Header and Locate Root

parse-header:
  require bytes[0..2] == 0xff 0x4a 0x5a
  version = byte[3]
  require version in {0x01, 0x02, 0x03, 0x04}
  flag1 = byte[4]
  flag2 = byte[5]
  reject reserved flag bits
  pos = 6
  if flag2 & 0x10:                         // top-level scalar
    read tree-node-segment-size
    tree-start = pos
    return root node at tree-start
  read primary-field-name-count using width selected by flag1/flag2
  read primary-field-name-heap-size using width selected by flag1
  if version in {0x03, 0x04}:
    read pflag3, pflag4, secondary-field-name-count, secondary-field-name-heap-size
    require reserved secondary flag bits are zero
  read tree-node-segment-size
  read tot-tiny-node or shared-node-segment offset
  read field-name dictionary segment
  tree-start = current position
  if version in {0x02, 0x04}:
    parse update extension after original tree-node segment

Decode Node at Tree Offset

decode-node(tree-offset):
  absolute = tree-start + tree-offset
  require absolute is inside the active tree segment
  opcode = byte[absolute]
  if version in {0x02, 0x04} and opcode is a forwarding marker:
    return resolve-forwarding-marker(tree-offset, opcode)
  if (opcode & 0xc0) == 0xc0:
    decode array node
  else if (opcode & 0xc0) == 0x80:
    decode object node
  else:
    decode scalar node by opcode table
  reject unknown opcodes, reserved opcodes, and out-of-range payload lengths

Object Field Lookup and Offset Resolution

object-get(object-offset, field-name):
  fid = dictionary lookup by hash, length, and UTF-8 bytes
  if fid is absent:
    return absent
  read object child count, FID array, and child-offset array
  if the object FID array is sorted:
    index = binary search FID array for fid
  else:
    index = linear search FID array for fid
  if index is absent:
    return absent
  raw = child-offset[index]
  if root flag2 has REL_OFFSET:
    raw = raw + object-offset
  return decode-node(raw)

A decoder MUST treat dictionary sizes, tree sizes, scalar payload lengths, and child offsets as untrusted input. It MUST bounds-check each computed address against the segment that owns the address before reading from it.

Validation Rules

  1. The magic and version are recognized.
  2. All offsets point inside the OSON payload or the active segment.
  3. Object FID arrays are sorted and have the same cardinality as child offset arrays.
  4. FIDs reference existing dictionary entries.
  5. Field-name lengths do not exceed the version's maximum length field.
  6. UTF-8 field names and JSON strings are well-formed when declared as UTF-8.
  7. Scalar payload lengths match the scalar opcode family.
  8. Forwarding offsets terminate at a concrete node.
  9. Shared-node references point to valid node records.
  10. Reserved flags and reserved opcode ranges are either ignored only where specified as ignorable, or rejected.

Conformance

Conformant Decoder

  1. A conformant decoder MUST verify the magic bytes and MUST accept versions 0x01, 0x02, 0x03, and 0x04.
  2. It MUST reject unrecognized versions, reserved root-header flag bits, reserved secondary-dictionary flag bits, reserved update-header flag bits, and reserved scalar opcodes unless a later public extension defines them.
  3. It MUST implement dictionary lookup using the stored field-name hash id, field-name length, and UTF-8 byte comparison, and MUST resolve FIDs across both primary and secondary dictionaries.
  4. It MUST decode all object, array, scalar, native-integer, extended-binary, and partial-update forwarding opcodes defined by this document.
  5. It MUST support relative-offset mode, FID-array sharing, shared-node images, and version 0x02/0x04 forwarding.
  6. It MUST bounds-check all offsets, lengths, child counts, and forwarding targets before dereferencing them.

Conformant Encoder

  1. A conformant encoder MUST emit the magic bytes and an appropriate version. It SHOULD use version 0x01 unless it needs the secondary dictionary or partial-update extension; it SHOULD use version 0x03 when any field name is longer than 255 bytes.
  2. It MUST set JZNOCT2_INLINE_LEAF and JZNOCT2_SLEN_IN_PCODE, and MUST set JZNOCT_HID_USEUB1 whenever the primary dictionary is non-empty.
  3. It MUST NOT set reserved root-header, secondary-dictionary, or update-header flag bits.
  4. It MUST build dictionaries using the specified FNV-1a hash, byte order, sort key, and FID numbering rules.
  5. It MUST emit multibyte integers in big-endian byte order and select width flags consistently with the emitted sizes.
  6. It MAY omit compact shared-node production, relative-offset production, and FID-array sharing; images without those optimizations remain conformant.

Interoperability Notes

The dictionary hash byte order, FID sort order, and baseline encoder flags are common sources of cross-implementation defects. An implementation SHOULD test images that use long field names, unsorted object FID arrays, relative offsets, shared FID arrays, and forwarded partial-update nodes, because those features exercise the decoder paths most likely to diverge between independent implementations.

Comparison With BSON

PropertyBSONOSON
Object field namesStored inline with each element.Stored once in a per-instance field-name dictionary and referenced by FID.
TraversalMostly sequential within an object.Tree offsets allow direct jump navigation.
Top-level valueHistorically document-oriented.Any JSON value can be top-level.
Partial updateNot a primary layout goal.Supported through node sizing, forwarding offsets, and update segments.
SQL scalar typesLimited extension set.Includes SQL/JSON scalar extensions such as dates, timestamps, intervals, binary values, and vectors.

Extension Rules

Future OSON versions can add scalar opcode families and flags.

  1. Writers SHOULD use the lowest version that can represent the value.
  2. Readers MUST reject unknown required versions.
  3. Readers SHOULD preserve unknown binary scalar payloads when operating in a lossless copy mode.
  4. Public extensions SHOULD define the opcode family, payload length rules, byte order, and comparison semantics.

Section 4. JSON Binary Tree Encoding Example

Given this JSON document:

{
  "person": {
    "id": "123",
    "name": "john",
    "birthdate": "1970-01-02",
    "friends": [
      { "person": { "id": "456", "name": "Mary", "birthdate": "1968-04-03" } },
      { "person": { "id": "789", "name": "Henry", "birthdate": "1972-03-03" } }
    ],
    "location": { "city": "Oakland", "zip": "94403" }
  }
}

A version 1 OSON image for this value can be summarized as follows:

ComponentExample valueMeaning
Magic0xff 0x4a 0x5aOSON document signature.
Version0x01Base instance format.
flag10x21Primary hash-id byte is present and tiny-node statistics are recorded.
flag20x06Inline scalar values and scalar lengths encoded in opcodes.
Total distinct field names8The dictionary has eight FIDs.
Field-name heap size51 bytesConcatenated field-name heap size.
Tree-node segment size143 bytesSerialized tree nodes for the value.
Tiny-node statistic1One tiny node is recorded for update planning.

The field-name dictionary stores each distinct field name once. The example uses one-byte hash ids and uint16 field-name offsets:

FIDHash-id byteField-name heap offsetField name
14215birthdate
26642city
31280person
416633location
518047zip
621825friends
72247id
823010name

The root node is an object with one field, person, whose FID is 3 and whose child node offset is 5. The person object then contains five fields: id, name, birthdate, friends, and location. The scalar string values use short-string opcodes where the low bits carry the string length.

nodeOff=0   OBJECT opcode=0x84 numFields=1 { fid=3,nodeOff=5 }
nodeOff=5   OBJECT opcode=0x84 numFields=5 { fid=7,nodeOff=22; fid=8,nodeOff=26; fid=1,nodeOff=31; fid=6,nodeOff=42; fid=4,nodeOff=121 }
nodeOff=22  SCALAR opcode=0x03 string len=3  "123"
nodeOff=26  SCALAR opcode=0x04 string len=4  "john"
nodeOff=31  SCALAR opcode=0x0a string len=10 "1970-01-02"
nodeOff=42  ARRAY  opcode=0xc0 numFields=2 [ nodeOff=48; nodeOff=84 ]
nodeOff=48  OBJECT opcode=0x86 field-id array referred, numFields=1 { fid=3,nodeOff=53 }
nodeOff=53  OBJECT opcode=0x84 numFields=3 { fid=7,nodeOff=64; fid=8,nodeOff=68; fid=1,nodeOff=73 }
nodeOff=64  SCALAR opcode=0x03 string len=3  "456"
nodeOff=68  SCALAR opcode=0x04 string len=4  "Mary"
nodeOff=73  SCALAR opcode=0x0a string len=10 "1968-04-03"
nodeOff=84  OBJECT opcode=0x9c object field-id reference, numFields=1 { fid=3,nodeOff=89 }
nodeOff=89  OBJECT opcode=0x84 numFields=3 { fid=7,nodeOff=100; fid=8,nodeOff=104; fid=1,nodeOff=110 }
nodeOff=100 SCALAR opcode=0x03 string len=3  "789"
nodeOff=104 SCALAR opcode=0x05 string len=5  "Henry"
nodeOff=110 SCALAR opcode=0x0a string len=10 "1972-03-03"
nodeOff=121 OBJECT opcode=0x84 numFields=2 { fid=2,nodeOff=129; fid=5,nodeOff=137 }
nodeOff=129 SCALAR opcode=0x07 string len=7  "Oakland"
nodeOff=137 SCALAR opcode=0x05 string len=5  "94403"

This example illustrates the main OSON navigation pattern: resolve a field name to its FID through the dictionary, binary-search or scan the object's FID array according to its sortedness flags, then jump directly to the matching child node offset.

Example

{"a": true, "b": [1, 2], "c": "cat"}
magic/version
field-name dictionary: ["a", "b", "c"]
root object:
  FID("a") -> scalar true
  FID("b") -> array node
      [0] -> scalar integer 1
      [1] -> scalar integer 2
  FID("c") -> scalar string "cat"

Machine-Readable Registry

The following JSON fragments summarize the flag and opcode assignments for code generators and decoder test tools. The prose and tables above are authoritative if there is any discrepancy.

Flags

{
  "byte_order": "big-endian",
  "magic": [255, 74, 90],
  "versions": {
    "1": "base",
    "2": "base+partial-update-extension",
    "3": "long-field-names",
    "4": "long-field-names+partial-update-extension"
  },
  "flag1": {
    "JZNOCT_HID_USEUB1": "0x01",
    "reserved": "0x02",
    "JZNOCT_TOT_DISFNM_UB2": "0x04",
    "JZNOCT_FLDNM_SZ_UB4": "0x08",
    "JZNOCT_TREE_SZ_UB4": "0x10",
    "JZNOCT_TINY_NODE_STAT": "0x20",
    "JZNOCT_HAS_SHR_NODES_SEG_CLUSTER": "0x40",
    "JZNOCT_FID_NO_SORT": "0x80"
  },
  "flag2": {
    "JZNOCT2_REL_OFFSET": "0x01",
    "JZNOCT2_INLINE_LEAF": "0x02",
    "JZNOCT2_SLEN_IN_PCODE": "0x04",
    "JZNOCT2_TOT_DISFNM_UB4": "0x08",
    "JZNOCT2_J_SCALAR": "0x10",
    "JZNOCT2_SHR_SIMP_NODES": "0x20",
    "JZNOCT2_SHR_NODES": "0x40",
    "reserved": "0x80"
  },
  "update_header": {
    "size": 16,
    "update_flag1": {"JZNOCTUPDHDR_OVFLW_SEG_UB2": "0x01"},
    "update_flag2": "reserved_zero",
    "overflow_entry_count_max": 1024,
    "overflow_map_size_max": 8192
  },
  "node_header": {
    "object_range": "0x80-0xbf",
    "array_range": "0xc0-0xff",
    "offset_size_bit": "0x20",
    "children_size_bits": "0x18",
    "fid_no_sort": "0x04",
    "fid_referred": "0x02",
    "update_object_overflow": "0x01"
  }
}

Scalar Opcodes

[
  {"range": "0x00-0x1f", "type": "string", "length": "opcode & 0x1f"},
  {"range": "0x20-0x2f", "type": "number", "length": "(opcode & 0x0f)+1"},
  {"op": "0x30", "type": "null"},
  {"op": "0x31", "type": "true"},
  {"op": "0x32", "type": "false"},
  {"op": "0x33", "type": "string", "length_prefix": "uint8"},
  {"op": "0x34", "type": "number", "length_prefix": "uint8"},
  {"op": "0x35", "type": "numeric_string", "length_prefix": "uint8"},
  {"op": "0x36", "type": "binary_double", "fixed_length": 8},
  {"op": "0x37", "type": "string", "length_prefix": "uint16"},
  {"op": "0x38", "type": "string", "length_prefix": "uint32"},
  {"op": "0x39", "type": "timestamp", "fixed_length": 11},
  {"op": "0x3a", "type": "binary", "length_prefix": "uint16"},
  {"op": "0x3b", "type": "binary", "length_prefix": "uint32"},
  {"op": "0x3c", "type": "date", "fixed_length": 7},
  {"op": "0x3d", "type": "interval_year_month", "fixed_length": 5},
  {"op": "0x3e", "type": "interval_day_second", "fixed_length": 11},
  {"range": "0x40-0x47", "type": "number_int32", "length": "opcode & 0x07"},
  {"range": "0x50-0x5f", "type": "number_int64", "length": "opcode & 0x0f"},
  {"range": "0x60-0x6f", "type": "decimal", "length": "(opcode & 0x0f)+1"},
  {"op": "0x74", "type": "decimal", "length_prefix": "uint8"},
  {"range": "0x75-0x77", "type": "partial_update_forwarding_marker"},
  {"op": "0x78", "type": "reserved_partial_update_growth_wrapper"},
  {"op": "0x79", "type": "native_integer_extension"},
  {"op": "0x7b", "type": "extended_binary", "subtypes": {"0x01": "vector", "0x02": "embedded_oson", "0x03": "scalar_array"}},
  {"op": "0x7c", "type": "timestamp_with_time_zone", "fixed_length": 13},
  {"op": "0x7d", "type": "timestamp7", "fixed_length": 7},
  {"op": "0x7e", "type": "id", "length_prefix": "uint8", "max_length": 127},
  {"op": "0x7f", "type": "binary_float", "fixed_length": 4},
  {"range": "0x80-0xbf", "type": "object_node"},
  {"range": "0xc0-0xff", "type": "array_node"}
]

Implementation References

Independent implementations can use this specification directly. Public Oracle client drivers are useful as reference implementations and test oracles for scalar payload handling, dictionary lookup, and node traversal behavior.

ImplementationNotesURL
python-oracledbOpen source Python driver with thin-mode JSON support.https://oracle.github.io/python-oracledb/
python-oracledb sourceSource repository for the Python driver.https://github.com/oracle/python-oracledb
Oracle JDBC driver JSON supportJava client implementation guidance and APIs for Oracle JSON values.https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/working-with-json-data.html

Background and Context Information

The following public references provide background on OSON, Oracle native JSON storage, and binary JSON format tradeoffs.

ReferenceSourceURL
SIGMOD 2016 paper: Closing the Functional and Performance Gap between SQL and NoSQLACM SIGMOD publication.https://dl.acm.org/doi/10.1145/2882903.2903731
VLDB 2020 paper: Native JSON Datatype Support: Maturing SQL and NoSQL Convergence in Oracle DatabasePVLDB paper PDF.https://www.vldb.org/pvldb/vol13/p3059-liu.pdf
Oracle Database blog: Autonomous JSON Database under the covers: OSON formatOracle Database blog.https://blogs.oracle.com/database/autonomous-json-database-under-the-covers-oson-format
A deep dive into Binary JSON formats: OSONMedium article.https://medium.com/db-one/a-deep-dive-into-binary-json-formats-oson-e3190e5e9eb0
Recorded talk on Oracle native JSON datatype and OSONYouTube recording.https://www.youtube.com/watch?v=_fChyzawOps