| Internet-Draft | JSON Three-Way Merge | August 2026 |
| Jurkovikj | Expires 5 February 2027 | [Page] |
For a fixed, disclosed resource policy, this document defines a deterministic three-way merge operation for a restricted JSON value domain. Given a shared base value and two independently derived values, called source and target, the operation produces either one complete merged JSON value or an ordered set of structured conflicts.¶
The operation defines strict JSON input processing, finite binary64 number normalization, scalar and object merge laws, explicit missing-member semantics, RFC 6901 conflict paths, typed conflict kinds, a fail-closed result for arrays, and bounded failure behavior. It is independent of HTTP and does not define array merge semantics, application-specific semantic resolution, content identity, or authorization policy.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 5 February 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Systems commonly retain a JSON value, allow two parties to edit copies of that value independently, and later need to reconcile both edits. Existing standardized JSON patch formats describe a change to one target value, but do not define how to reconcile two complete edited values derived from one shared base.¶
Without a common operation, implementations often use textual merge, last-writer-wins replacement, or application-specific rules. Those approaches can produce different outcomes across runtimes or silently discard one party's change.¶
This document defines an implementation-independent three-way merge operation:¶
Merge(base, source, target) -> merged value | ordered conflicts | typed input, unsupported-domain, or resource failure¶
The operation is deliberately conservative. It merges normalized scalar values and recursively array-free objects. Arrays are outside the initial merge domain and cause a typed fail-closed result even when all three arrays are equal. A future specification can define array behavior under a separate semantic identity and separate conformance vectors.¶
This document provides:¶
deterministic normalized equality for JSON values in the merge domain;¶
merge laws that preserve one-sided and agreed changes;¶
recursive combination of independent object-member changes;¶
explicit distinction between a missing member and a present JSON null;¶
conflicts for divergent edits, incompatible type changes, divergent additions, and edit-versus-delete cases;¶
unambiguous RFC 6901 paths and deterministic conflict ordering; and¶
bounded, complete outcomes without partial merged values or conflict prefixes.¶
This document does not define:¶
array merge, sequence alignment, keyed collections, or reordering rules;¶
a JSON diff or patch-generation algorithm;¶
a CRDT, operational transformation, consensus, or replication protocol;¶
application-specific semantic merge or automatic conflict resolution;¶
provenance receipts, redaction formats, or migration between merge engines;¶
JSON canonicalization or content identifiers as a merge prerequisite;¶
an HTTP method, field, status code, Link relation, or media type;¶
persistence, atomic commit, authentication, authorization, or licensing; or¶
a determination that a structurally merged value is valid for an application's schema or policy.¶
JSON Patch [RFC6902] expresses an ordered sequence of operations to apply to one target document. JSON Merge Patch [RFC7396] expresses, by example, a patch to apply to one target value. Both are useful mutation formats, but neither takes a shared base and two independently edited complete values or defines the conflicts specified here.¶
JSON Pointer [RFC6901] defines the path syntax used by this document. JSON [RFC8259] defines the underlying interchange syntax and data model, subject to the additional input and number restrictions in this document.¶
Internet JSON (I-JSON) [RFC7493] has related interoperability goals. Both I-JSON and this document require UTF-8, reject duplicate decoded object names, exclude lone surrogates, and account for binary64 interoperability. This document is not an I-JSON profile, however: it recursively excludes arrays, normatively converts admitted numbers to finite binary64 values, and does not import every I-JSON restriction, including its prohibition on Unicode noncharacters.¶
The JSON Canonicalization Scheme (JCS) [RFC8785] defines deterministic JSON serialization for hashing and signing. This merge operation is deterministic at the normalized JSON-value and ordered-diagnostic levels and does not require JCS, a digest, or a content identifier. An application that independently needs deterministic bytes can apply a compatible canonicalization contract after a successful merge. The unsigned UTF-16 comparator used here for one decoded pointer token aligns with JCS property-name sorting, but this document's ordering of complete pointer-token sequences is separate and does not make JCS a merge prerequisite.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
The shared JSON value from which source and target were independently derived.¶
One edited JSON value derived from the base. The name does not grant this side authority over target.¶
The other edited JSON value derived from the base. The name does not grant this side authority over source.¶
A value admitted by the input-domain rules (Section 4), with every number represented by its normalized finite binary64 value.¶
An object member exists and has a JSON value, including JSON null.¶
An object member does not exist. Missing is an operation-internal state and is not a seventh JSON value kind.¶
The base, source, and target root values are always present. Missing can arise only while processing an object member.¶
A stable identifier for one fixed set of resource ceilings, deterministic
measurement encodings, and any policy-defined limit precedence. The generic
JSON result form carries this identifier in error.budget.¶
An implementation of the operation defined by this document.¶
In pseudocode, B, S, and T denote base, source, and target values.
M(B,S,T) denotes the merge operation. == denotes normalized equality as
defined in Normalized Equality and Kind (Section 4.4), not textual
equality of input JSON.¶
Each input is a UTF-8 sequence containing exactly one JSON value as defined by [RFC8259]. Leading and trailing JSON whitespace are permitted. After the value and trailing whitespace, any additional octet is invalid.¶
A leading UTF-8 byte order mark (the octet sequence encoding U+FEFF) MUST be
rejected with code invalid_json. This profile does not use the permissive
RFC 8259 option to ignore a byte order mark while parsing.¶
A merge processor MUST reject malformed JSON, invalid UTF-8, and escaped or unescaped lone surrogate code points. It MUST preserve admitted Unicode scalar values exactly and MUST NOT apply Unicode normalization.¶
An object MUST NOT contain the same decoded member name more than once. Escape
spelling does not make names distinct; for example, "a" and "\u0061" are
duplicates in the same object and MUST be rejected before merge processing.¶
Many general-purpose JSON parsers silently retain only the first or last occurrence of a duplicate member name. A processor cannot satisfy the requirement above by inspecting a post-parse object after such a collapse. It MUST use a parser mode or input front end that exposes every decoded member-name occurrence or rejects duplicates before collapsing them.¶
Every JSON number token MUST be converted to the nearest IEEE 754 binary64
value [IEEE754] using round-to-nearest, ties-to-even. A token whose
conversion overflows to positive or negative infinity MUST be rejected. NaN
and infinity are not JSON number tokens and are invalid input.¶
All finite binary64 values, including subnormal values, are admitted. Positive
and negative zero compare equal and are normalized to positive zero. Thus, the
input tokens -0, 0, and 0.0 denote the same normalized number.¶
Different decimal tokens that convert to the same binary64 value are equal.
For example, 1, 1.0, and 1e0 are equal. The token
9007199254740993 converts to the binary64 value also denoted by
9007199254740992; those tokens therefore compare equal under this profile.
Applications that require exact integers beyond binary64 precision MUST encode
them under an application-defined representation, such as JSON strings, rather
than rely on this merge operation to preserve their decimal-token distinction.¶
This document's acceptance and normalization of negative zero are explicit. They do not impose the stricter negative-zero parser behavior discussed by verified Erratum 7920 for [RFC8785], because JCS conformance is not a merge input requirement here.¶
The merge domain contains these normalized JSON value kinds:¶
null;¶
boolean;¶
number, represented by the binary64 number model (Section 4.2);¶
string; and¶
object whose values are recursively in this domain.¶
Arrays are recursively unsupported. An array at the root or below an object member prevents that complete input from entering the merge engine.¶
Two normalized values are equal when:¶
both are null;¶
both are the same boolean;¶
both are the same normalized binary64 value;¶
both are strings containing the same sequence of Unicode scalar values; or¶
both are objects with the same decoded member-name set and recursively equal values for every member.¶
Object member order in input JSON does not affect equality. No Unicode normalization, case folding, locale comparison, or application-specific coercion is performed.¶
The five merge value kinds above are distinct. In particular, a number is not equal to a string containing its decimal spelling, and null is not equal to a missing object member.¶
The merge processor MUST process complete inputs in this order:¶
It MUST stop at the first side that produces an invalid-input, unsupported-domain, or resource-limit result. The semantic merge engine runs only after all three inputs are valid, normalized, recursively array-free, and admitted by the active resource policy.¶
For example, an invalid base wins over an unsupported array in source, and an unsupported array in source wins over an invalid target.¶
Within one side, processing is bounded and left-to-right. The processor MUST use this precedence at each parser event:¶
Check the raw input-octet ceiling before scanning.¶
Before entering a recognized container, check nesting depth and node count.¶
Before constructing a recognized scalar, check node count.¶
When an object member begins, check member count before decoding and retaining its name; check decoded name size while decoding it.¶
Detect a duplicate decoded member name before parsing that member's value.¶
Before parsing an array child, check any per-array element ceiling and the document node ceiling.¶
Check decoded string size and raw number-token size while scanning.¶
Report syntax, Unicode, and numeric-domain failures at the first point they become knowable.¶
If the active policy defines a complete normalized-input byte ceiling, compute it using the deterministic encoding named by that policy and check it before admitting the side.¶
If malformed syntax becomes knowable before a resource boundary is crossed,
status invalid_input wins. If a bounded token reaches the first prohibited
octet before a defect in an unread suffix becomes knowable, status
resource_limit wins. Unsupported-array selection occurs only after the side
is otherwise completely valid and admitted.¶
For one side, the processor first completes bounded JSON and number validation.
If the valid side contains one or more arrays, it MUST return status
unsupported with:¶
code arrays_not_supported;¶
side base, source, or target; and¶
the RFC 6901 path of the least array under the path ordering (Section 7.3).¶
The root array path is the empty string. Equal, unchanged, or empty arrays are still unsupported. A processor MUST NOT apply an equality shortcut before array-domain validation.¶
The selected path is the least path under that ordering, not necessarily the first array encountered in the raw JSON text. A processor MAY maintain a running minimum during validation or compare retained candidate paths after validation; it MUST NOT substitute raw encounter order for the required path ordering.¶
The least selected array cannot be reached through an array element. If an array had an ancestor array, the ancestor's pointer-token sequence would be a strict prefix and would sort first. Consequently, the selected array is either the root or is reached only through object-member steps. Numeric-looking object member names remain ordinary pointer tokens and are compared by the same token rules.¶
An implementation MAY validate unsupported array content without retaining the complete array, but it MUST still detect malformed content, duplicate object names, invalid numbers, and resource failures according to the rules above.¶
An invalid-input result uses one of these codes:¶
invalid_json for malformed JSON, invalid UTF-8, trailing content, or an
invalid Unicode scalar sequence;¶
duplicate_object_name for a repeated decoded name in one object; or¶
number_out_of_binary64_range for a number that cannot be represented as a
finite binary64 value.¶
The result MUST identify the failing input side. It MUST contain an RFC 6901
path when the affected value or containing object is known. For
duplicate_object_name, the path MUST identify the containing object; the
duplicated name is not appended as a child token. For
number_out_of_binary64_range, the path MUST identify the offending number
value itself. A processor MAY include a zero-based UTF-8 byte offset and
derived line and column information. Such optional source-location details do
not alter the merge outcome.¶
Every merge execution MUST be subject to a finite resource policy. The policy MUST bound, at minimum:¶
raw octets per input side;¶
container nesting depth;¶
total value nodes and object-member occurrences per input;¶
array elements processed while validating an unsupported array;¶
decoded member-name and string-value octets;¶
number-token octets;¶
retained conflicts; and¶
any serialized result retained by the processor.¶
A policy MAY additionally bound the bytes in each complete normalized input. Such a bound is distinct from raw-input, token, node, member, and serialized result limits. The policy MUST identify the deterministic normalized-value encoding used for that measurement and the corresponding limit code. This document neither selects nor requires a JSON canonicalization scheme for that purpose.¶
A processor MUST associate its fixed ceilings with a stable resource-policy identifier. If any ceiling changes, the identifier MUST also change. Limits are implementation policy rather than universal ceilings defined by this document, but they are an explicit input to reproducible operation behavior. An invocation MUST NOT silently select an unbounded path or raise a ceiling based on attacker-controlled input.¶
A resource-limit result MUST identify the resource-policy identifier, limit
code, charged side, path, inclusive limit, and observed value. To permit early
failure without scanning or retaining a hostile suffix, observed SHOULD be
reported as the first prohibited value, limit + 1.¶
A processor can define additional limit codes. The following code spellings SHOULD be used when the corresponding resource is bounded:¶
input_bytes nesting_depth total_nodes total_object_members array_length key_bytes string_bytes number_token_bytes normalized_document_bytes result_bytes merge_response_bytes conflict_count¶
When a policy uses one of those suggested codes, the resource-limit path
MUST be selected as follows:¶
| Code | Failure path |
|---|---|
input_bytes
|
Root, because the check occurs before input scanning. |
nesting_depth
|
The would-be container path. |
total_nodes
|
The would-be value path. |
total_object_members
|
The containing object path. |
array_length
|
The would-be element path, including its zero-based array index. |
key_bytes
|
The containing object path; the oversized name is not admitted. |
string_bytes
|
The string value path. |
number_token_bytes
|
The number value path. |
normalized_document_bytes
|
The first value or member, in the policy's deterministic normalized-value encoding order, whose byte contribution crosses the ceiling; container punctuation uses the containing-container path. |
result_bytes
|
The first value or member, in deterministic merge-result encoding order, whose byte contribution crosses the ceiling; container punctuation uses the containing-container path. |
merge_response_bytes
|
The first unadmitted conflict path; root if a fixed envelope or a non-conflict response component crosses the ceiling. |
conflict_count
|
The first unadmitted conflict path. |
The path-attribution rule for an additional limit code MUST be defined by the resource policy. Encoding-derived measurements MUST attribute each measured byte contribution in a deterministic order. Path-attribution rules are part of the resource policy; changing one requires a new resource-policy identifier.¶
result_bytes measures the deterministic policy-selected serialization of a
complete normalized merged value, excluding the generic result envelope. It
applies only after conflict discovery has established that the operation is
conflict-free. merge_response_bytes measures the deterministic
policy-selected serialization of the complete generic result form or of a
fixed adopting envelope, including status-specific conflicts or error detail.
A policy that uses either code MUST identify the exact encoding being measured
and, for merge_response_bytes, the exact envelope and included members. If
both codes apply to one successful operation, result_bytes MUST be checked
before merge_response_bytes. Output-side failures use side merge_result.¶
Depth counts open object and array containers: a scalar root has depth zero, a root container has depth one, and entering a child container increments depth. Every JSON value, including the root and every unsupported array container, counts as one node. Each syntactically started object-member occurrence counts once, including a later-rejected duplicate. Name and string ceilings count decoded UTF-8 octets; number-token ceilings count the raw token from an optional minus sign through the final digit of the token, excluding whitespace.¶
Resource limits MUST be checked before the allocation, insertion, recursive descent, clone, or output append that would exceed the active ceiling. Integer accounting MUST detect overflow rather than wrap.¶
A processor claiming conformance to this document MUST configure limits high enough to execute every applicable conformance vector (Appendix B).¶
After all three inputs are admitted, each value merge applies these normalized equality shortcuts in order:¶
if S == T: return S if B == S: return T if B == T: return S¶
Consequently, over the recursively array-free domain, the operation satisfies:¶
M(B, B, B) = B M(B, S, B) = S M(B, B, T) = T M(B, X, X) = X¶
Swapping source and target preserves a successful merged value and every conflict path. The two oriented edit-versus-delete conflict kinds swap as described in Conflict Kinds (Section 7.1).¶
If no equality shortcut applies and all three values are objects, the object operation in Object Merge (Section 6.3) applies.¶
Otherwise, the processor MUST emit one conflict at the current path:¶
concurrent_value_change when all three values have the same merge value
kind and they are not all objects; or¶
concurrent_type_change when their merge value kinds are not all the same.¶
The processor MUST NOT choose source or target based on role name, textual ordering, timestamp, input member order, or serialization length.¶
If all three values at a path are objects and no equality shortcut applies, the processor considers the union of their decoded member names. It processes those names in the unsigned UTF-16 order defined in Path and Conflict Ordering (Section 7.3) and applies the presence matrix (Section 6.4) to each member.¶
When all three member states are present, the processor recursively applies the value merge at the child path. Independent changes under different member names therefore combine into one object.¶
The processor constructs a complete merged object only when no conflict exists anywhere in the operation. If any conflict exists, non-conflicting provisional work is discarded and the result contains only the complete ordered conflict set.¶
Conflict discovery and conflict-diagnostic limit checks MUST precede
construction or certification of a merged result. A processor MUST NOT return
a merged-result node, member, depth, encoding, or serialized-size limit while
a semantic conflict remains to be discovered. Those merged-result limits apply
only after the operation is known to be conflict-free. Conflict-count and
conflict-response limits can still produce a complete resource_limit result
during bounded conflict discovery.¶
Presence equality compares both state and, for present states, normalized value. The following matrix summarizes complete object-member behavior, including outcomes selected by the same three equality shortcuts:¶
In the table, present B in any column means present with a normalized value
equal to the base value denoted by B; it does not require identical input
spelling. S, T, and X likewise denote normalized values, and != denotes
normalized inequality.¶
| Base | Source | Target | Outcome |
|---|---|---|---|
| present B | missing | present B | missing |
| present B | present B | missing | missing |
| present B | missing | present T, T != B |
source_delete_target_edit conflict |
| present B | present S, S != B | missing |
source_edit_target_delete conflict |
| present B | missing | missing | missing |
| missing | present S | missing | present S |
| missing | missing | present T | present T |
| missing | present X | present X | present X |
| missing | present S | present T, S != T |
add_add conflict |
If all three states are present, the merge operation (Section 6)
recursively merges their values.
If base is missing and source and target add different objects under the same
member name, the processor reports add_add at that member path. It MUST NOT
invent an empty base object and combine the divergent additions recursively.¶
Every invocation produces exactly one complete status. A conflict result MUST NOT contain a merged value. A merged result MUST NOT contain conflicts. An invalid-input, unsupported, or resource-limit result MUST NOT contain a merged value, a conflict prefix, parser output, or partially normalized value.¶
If conflict accumulation or conflict-response measurement reaches a resource ceiling during conflict discovery, the processor MUST discard the partial collection and return one complete resource-limit result. If the operation is conflict-free and merged-result construction or certification reaches a resource ceiling, it MUST discard the provisional result and return one complete resource-limit result.¶
The conflict kinds are:¶
concurrent_value_changeBase, source, and target are present with the same merge value kind, they are not all objects, and both sides differ from base and from each other.¶
concurrent_type_changeBase, source, and target are present; no equality shortcut applies; and the three merge value kinds are not all the same.¶
add_addThe member is missing from base and both sides add unequal normalized values.¶
source_delete_target_editThe member is present in base, source deletes it, and target changes it to a value unequal to base.¶
source_edit_target_deleteThe member is present in base, source changes it to a value unequal to base, and target deletes it.¶
Every conflict contains one kind, one RFC 6901 path, and explicit base, source, and target presence states. It MUST contain the complete normalized value for each present state unless an adopting application defines a separate, non-conforming redacted diagnostic view. A redacted view cannot be used as the input to automatic conflict resolution without application-specific rules.¶
In the JSON result form, presence is encoded as:¶
{"state":"missing"}
¶
or:¶
{"state":"present","value":<normalized-json-value>}
¶
The value member MUST be absent when state is missing and MUST be present
when state is present. A present value can be JSON null.¶
Paths are RFC 6901 JSON Pointers. The document root is the empty string. For a
member name, ~ is escaped as ~0 and / is escaped as ~1.¶
Ordering compares decoded pointer-token sequences, not rendered pointer strings. Each token is compared lexicographically as a sequence of unsigned UTF-16 code units. If all units in the shorter token are equal to the prefix of the longer token, the shorter token sorts first. If all shared tokens are equal, the pointer with fewer tokens sorts first.¶
This comparison is locale-independent and does not apply Unicode normalization. A supplementary Unicode scalar value is compared through its UTF-16 surrogate pair. Lone surrogates cannot occur because they are rejected during input processing.¶
UTF-16 code-unit order deliberately differs from Unicode scalar-value order for some comparisons between supplementary and Basic Multilingual Plane characters. An implementation whose native string ordering uses scalar values or UTF-8 bytes therefore MUST explicitly derive the UTF-16 code-unit order rather than use its runtime's default string comparison.¶
The operation defined by this document produces at most one conflict for one logical path. The conflict array MUST be sorted by the path ordering above. Object-member traversal and least-array selection MUST use the same ordering.¶
This section defines a generic JSON representation of an operation result. It
does not register a media type or require an application to transfer results
over a network. An adopting specification MAY wrap these members in a
versioned envelope, but MUST preserve their meanings. The ordering requirement
applies to entries in the conflicts array, not to JSON object members.¶
JSON object member order and insignificant whitespace in this result form are not significant. Every emitted number value MUST represent the corresponding normalized binary64 value when parsed under the binary64 number model (Section 4.2), and negative zero MUST NOT be emitted. When serialized, this result form MUST use UTF-8 and MUST NOT begin with a byte order mark.¶
Every generic result MUST contain the string member status and exactly the
status-specific top-level member shown below. The other two status-specific
members in the table MUST be absent. Additional envelope or error-detail
members remain permitted as described below.¶
| status | Required top-level member | Forbidden top-level members |
|---|---|---|
merged
|
merged
|
conflicts, error
|
conflict
|
conflicts
|
merged, error
|
unsupported
|
error
|
merged, conflicts
|
invalid_input
|
error
|
merged, conflicts
|
resource_limit
|
error
|
merged, conflicts
|
{
"status": "merged",
"merged": {"name":"ready","owner":"B"}
}
¶
The merged member MUST contain the complete normalized merged value.¶
{
"status": "conflict",
"conflicts": [
{
"kind": "concurrent_value_change",
"path": "/title",
"base": {"state":"present","value":"Draft"},
"source": {"state":"present","value":"Ready"},
"target": {"state":"present","value":"Review"}
}
]
}
¶
The conflicts array MUST be non-empty and ordered as specified in
Path and Conflict Ordering (Section 7.3).¶
{
"status": "unsupported",
"error": {
"code": "arrays_not_supported",
"side": "source",
"path": "/items"
}
}
¶
{
"status": "invalid_input",
"error": {
"code": "duplicate_object_name",
"side": "base",
"path": ""
}
}
¶
{
"status": "resource_limit",
"error": {
"budget": "example-resource-budget-v1",
"code": "nesting_depth",
"side": "target",
"path": "/configuration",
"limit": 64,
"observed": 65
}
}
¶
The required error members are:¶
| status | Required error members | Additional defined members |
|---|---|---|
unsupported
|
code, side, path
|
none |
invalid_input
|
code, side; path when known |
byte_offset, line, column
|
resource_limit
|
budget, code, side, path, limit, observed
|
none |
For unsupported and invalid_input, side is base, source, or
target. For resource_limit, those values identify an input-side charge and
merge_result identifies conflict or result construction. In a
resource_limit error, budget is a string containing the
resource-policy identifier (Section 5.5). Both limit and observed
are non-negative integers. Status-specific codes and path rules are defined in
Validation and Failure Precedence (Section 5).¶
An implementation can add envelope or error-detail members, but MUST NOT use a defined status, conflict kind, or error code with a different meaning.¶
Given:¶
base = {"name":"draft","owner":"A"}
source = {"name":"ready","owner":"A"}
target = {"name":"draft","owner":"B"}
¶
source changes name, target changes owner, and neither change overlaps.
The merged value is:¶
{"name":"ready","owner":"B"}
¶
Given an empty base, source adds a present null member and target remains unchanged:¶
base = {}
source = {"reviewed":null}
target = {}
¶
The merged value is {"reviewed":null}. The member is not deleted because a
present JSON null is a value, not the missing state.¶
Given:¶
base = {"title":"Draft"}
source = {}
target = {"title":"Ready"}
¶
the result contains source_delete_target_edit at /title, with source
encoded as missing and the other two states encoded as present.¶
Given:¶
base = {}
source = {"settings":{"left":true}}
target = {"settings":{"right":true}}
¶
the result is add_add at /settings. Although the two added objects contain
different member names, they have no shared base object. This operation does
not invent one and silently combine both additions.¶
Given:¶
base = {"count":1}
source = {"count":1.0}
target = {"count":2}
¶
base and source are equal after number normalization, so the merged value is
{"count":2}.¶
This merge operation does not depend on HTTP. An HTTP application can use it after a conditional state-changing request fails because the resource changed [RFC9110]:¶
base is the representation state previously observed by the client;¶
source is the client's complete proposed state derived from base;¶
target is freshly obtained current state;¶
the application runs this merge operation;¶
conflicts are reviewed or resolved under application policy; and¶
a successful candidate is validated and submitted using a precondition for the newly observed current state.¶
A successful structural merge does not authorize a blind retry. The application still needs current-state validation, authorization, schema and policy checks, and an atomic commit mechanism. Agentic State Transfer [I-D.jurkovikj-httpapi-agentic-state] is an example of an HTTP profile whose application layer can perform reconciliation after a precondition failure; it does not normatively depend on this document.¶
JSON depth, width, token size, and conflict count are attacker-controlled. Processors MUST enforce the resource policy (Section 5.5) before prohibited recursion or allocation. Rejecting arrays does not permit unbounded parsing merely to discover that an array is unsupported.¶
A conforming conflict contains values from base, source, and target. Those values can include credentials, personal information, deleted content, configuration secrets, or fields hidden from one participant. Conflict results MUST be treated as sensitive application data. Authentication and authorization checks MUST precede disclosure. Implementations MUST NOT log, render, or transmit raw conflicts by default when doing so would expose data to an unauthorized party. An authorized interface that presents conflict values MUST encode them as untrusted structured data for the destination context and MUST NOT interpret them as HTML, terminal controls, executable content, or format strings.¶
An application can create a redacted human-facing view, but that view is not a conforming complete conflict and might omit information required for safe resolution.¶
This operation detects structural overlap under its fixed rules. It does not know application invariants. Independent edits can merge structurally while violating a schema, cross-field constraint, business rule, authorization condition, or safety policy. Applications MUST validate the complete candidate under current policy before commit.¶
Automated agents MUST NOT interpret status merged as approval or authority to
perform an external action.¶
An incorrect or attacker-selected base can make concurrent changes appear one-sided. Applications need an authenticated way to associate base with the states actually observed by both editors. In concurrent systems, current-state validation and candidate commit need to be atomic or protected by an appropriate compare-and-swap mechanism.¶
Decimal tokens can collapse to one binary64 value, including large integers outside the exactly representable range. Applications MUST NOT use this merge number model for identifiers, account balances, counters, or other values that require decimal or arbitrary-precision distinction unless their application representation encodes that distinction explicitly.¶
No Unicode normalization is performed. Visually similar member names can remain distinct. Interfaces SHOULD display escaped paths and untrusted member names in a form that prevents control-character injection, bidirectional-text confusion, and HTML or terminal interpretation. Processors MUST compare decoded logical pointer tokens, not presentation strings.¶
Treating an unsupported result as permission to fall back to last-writer-wins, index-wise merge, or an older unversioned array algorithm can silently discard data. Applications SHOULD preserve the last known good state and require a separately identified array-aware profile or explicit review.¶
This document has no IANA actions.¶
This section follows the guidance in [RFC7942]. It records implementation information as of 2026-08-04 and is expected to be removed before publication as an RFC.¶
libdualnative provides a Rust implementation of the scalar/object operation,
typed conflicts, array rejection, and fixed resource failures specified here.
Its policy-specific boundary suite exercises exact and first-prohibited
ceilings, failure precedence, conflict atomicity, and no-partial-result
behavior.
Its WebAssembly package is a facade over the same Rust engine and is parity
evidence, not an independent implementation. The experimental
v0.1.0-alpha.1 GitHub prerelease is available at
https://github.com/DualNative/libdualnative-v1/releases/tag/v0.1.0-alpha.1.
The release source commit is
d986f1ab9b851dea8dd8d4a99dac6ef92acd8f25.¶
The implementation also exposes a separate RFC 8785 and SHA-256 content identity operation. That operation is not required by this document and is not evidence of an independent merge implementation.¶
At the time of writing, no complete, independently maintained implementation
in another language is known. The machine-readable companion suite contains 41
vectors and has SHA-256 digest
13467e8bf70718bdf247da4b007730e4f0e560f538db6a86f92a88a457c35e62.
During draft preparation, the Rust reference implementation was run against
all 41 companion vectors. A temporary JavaScript cross-check independently
exercised the 33 successful-merge, conflict, and unsupported-array vectors. It
did not implement strict input-error or resource-limit processing and is not a
complete or maintained implementation. A complete independent implementation
and common-vector execution are desirable before seeking working-group
adoption.¶
The following pseudocode is explanatory. Normative behavior is defined in the preceding sections.¶
MERGE-VALUE(B, S, T, path):
if EQUAL(S, T): return VALUE(S)
if EQUAL(B, S): return VALUE(T)
if EQUAL(B, T): return VALUE(S)
if OBJECT(B) and OBJECT(S) and OBJECT(T):
return MERGE-OBJECT(B, S, T, path)
if KIND(B) == KIND(S) and KIND(S) == KIND(T):
return CONFLICT(concurrent_value_change, path,
PRESENT(B), PRESENT(S), PRESENT(T))
return CONFLICT(concurrent_type_change, path,
PRESENT(B), PRESENT(S), PRESENT(T))
MERGE-MEMBER(b, s, t, path):
if EQUAL-PRESENCE(s, t): return s
if EQUAL-PRESENCE(b, s): return t
if EQUAL-PRESENCE(b, t): return s
match (b, s, t):
(PRESENT(B), PRESENT(S), PRESENT(T)):
return MERGE-VALUE(B, S, T, path)
(MISSING, PRESENT(S), PRESENT(T)):
return CONFLICT(add_add, path, MISSING,
PRESENT(S), PRESENT(T))
(PRESENT(B), MISSING, PRESENT(T)):
return CONFLICT(source_delete_target_edit, path,
PRESENT(B), MISSING, PRESENT(T))
(PRESENT(B), PRESENT(S), MISSING):
return CONFLICT(source_edit_target_delete, path,
PRESENT(B), PRESENT(S), MISSING)
MERGE-OBJECT(B, S, T, path):
provisional = empty object
conflicts = empty sequence
for key in SORT-UTF16(UNION-OF-NAMES(B, S, T)):
child = MERGE-MEMBER(PRESENCE(B, key),
PRESENCE(S, key),
PRESENCE(T, key),
APPEND-POINTER(path, key))
if child is conflict collection:
append child conflicts
else if child is present:
provisional[key] = child.value
if conflicts is not empty:
discard provisional
return SORT-BY-POINTER(conflicts)
return provisional
¶
The inputs in this appendix are JSON texts. Expected merged values and conflict presence values are compared after normalization, so insignificant whitespace, object member order, and equivalent binary64 number spelling do not alter a vector result.¶
A machine-readable companion suite maintained with the draft source is named
draft-jurkovikj-json-three-way-merge-00-vectors.json and uses schema
identifier draft-jurkovikj-json-three-way-merge-00-vectors-v1. Its base,
source, and target members are outer JSON strings whose decoded contents
are the exact UTF-8 input text supplied to the processor. This permits
malformed JSON text, a leading byte order mark, and an escaped lone surrogate
to remain expressible inside a valid suite file. A truly invalid UTF-8 octet
sequence requires a separate byte-oriented suite. Each expected object uses
the generic result-form names defined by this document; comparisons of merged
and conflict values occur after normalization.¶
Concrete resource ceilings are implementation policy, so this appendix does not assign one universal N/N+1 resource-limit vector. Policy-specific suites can add boundary vectors bound to their resource-policy identifier and exact ceilings.¶
no-change: base {"x":1}; source {"x":1.0}; target
{"x":1e0}; merged {"x":1}.¶
source-only: base {"x":1}; source {"x":2}; target {"x":1};
merged {"x":2}.¶
target-only: base {"x":1}; source {"x":1}; target {"x":2};
merged {"x":2}.¶
agreement: base {"x":1}; source {"x":2}; target {"x":2.0};
merged {"x":2}.¶
independent-members: base {"a":0,"b":0}; source
{"a":1,"b":0}; target {"a":0,"b":2}; merged {"a":1,"b":2}.¶
source-delete: base {"x":1}; source {}; target {"x":1};
merged {}.¶
target-delete: base {"x":1}; source {"x":1}; target {};
merged {}.¶
both-delete: base {"x":1}; source {}; target {}; merged {}.¶
one-sided-add-null: base {}; source {"x":null}; target {};
merged {"x":null}.¶
target-only-add: base {}; source {}; target {"x":1}; merged
{"x":1}.¶
agreed-add: base {}; source {"x":1}; target {"x":1.0}; merged
{"x":1}.¶
agreed-type-change: base 1; source {"x":2}; target {"x":2};
merged {"x":2}.¶
one-sided-type-change: base 1; source 1.0; target {"x":2};
merged {"x":2}.¶
large-integer-normalization: base 9007199254740992; source
9007199254740993; target 3; merged 3.¶
negative-zero-normalization: base {"x":-0}; source {"x":0.0};
target {"x":-0.0}; merged {"x":0}. A serialized merged result MUST NOT contain negative zero.¶
subnormal-equality: base 5e-324; source 4.9e-324; target 1;
merged 1 because the first two tokens convert to the same binary64 value.¶
Each expected conflict below is the sole conflict unless an ordered path list is shown.¶
scalar-divergence: base 1; source 2; target 3; kind
concurrent_value_change; path "".¶
type-divergence: base 1; source "source"; target {"x":2}; kind
concurrent_type_change; path "".¶
add-add: base {}; source {"x":1}; target {"x":2}; kind
add_add; path /x.¶
delete-edit: base {"x":1}; source {}; target {"x":2}; kind
source_delete_target_edit; path /x.¶
edit-delete: base {"x":1}; source {"x":2}; target {}; kind
source_edit_target_delete; path /x.¶
divergent-added-objects: base {}; source {"x":{"a":1}}; target
{"x":{"b":2}}; kind add_add; path /x.¶
null-versus-delete: base {"x":1}; source {"x":null}; target {};
kind source_edit_target_delete; path /x.¶
add-add-null: base {}; source {"x":null}; target {"x":1}; kind
add_add; path /x.¶
nested-type-conflict: base {"a":{"b":1}}; source
{"a":{"b":"source"}}; target {"a":{"b":true}}; kind
concurrent_type_change; path /a/b.¶
The decoded-token-sequence-order vector distinguishes decoded token-sequence
ordering from rendered-pointer-string ordering:¶
{
"base": {"a":{"b":0},"a!b":0},
"source": {"a":{"b":1},"a!b":1},
"target": {"a":{"b":2},"a!b":2},
"expected_status": "conflict",
"expected_paths": ["/a/b","/a!b"]
}
¶
The first decoded token a is a prefix of a!b, so /a/b sorts first.
Comparing the rendered pointer strings would incorrectly put /a!b first
because ! precedes /.¶
The empty-member-name-order vector uses base {"":0,"a":0}, source
{"":1,"a":1}, and target {"":2,"a":2}. Its expected conflict paths
are ["/","/a"]; the first pointer contains one empty decoded token.¶
The escaped-path-order vector checks escaped paths and deterministic
ordering:¶
{
"base": {"z":0,"a/b":0,"m~n":0},
"source": {"z":1,"a/b":1,"m~n":1},
"target": {"z":2,"a/b":2,"m~n":2},
"expected_status": "conflict",
"expected_paths": ["/a~1b","/m~0n","/z"]
}
¶
All three conflicts have kind concurrent_value_change.¶
The utf16-path-order vector makes the UTF-16 ordering rule visible when a
supplementary character and a Basic Multilingual Plane character are compared:¶
{
"base": {"\ue000":0,"\ud83d\ude00":0},
"source": {"\ue000":1,"\ud83d\ude00":1},
"target": {"\ue000":2,"\ud83d\ude00":2},
"expected_status": "conflict",
"expected_paths": ["/\ud83d\ude00","/\ue000"]
}
¶
The grinning-face character sorts first because its leading UTF-16 code unit
is 0xD83D, which is less than 0xE000.¶
root-array: base []; source {}; target {}; side base; path "".¶
nested-array: base {"items":null}; source {"items":[1]}; target
{"items":null}; side source; path /items.¶
equal-arrays: all three inputs are {"items":[1]}; side base; path
/items.¶
least-array-path: base {"z":[],"a":{"items":[]}}; source {};
target {}; side base; path /a/items.¶
invalid-base-first: base {broken; source []; target {}; status
invalid_input; side base; code invalid_json; path "".¶
source-array-before-target: base {}; source []; target {broken;
status unsupported; side source; code arrays_not_supported.¶
decoded-duplicate: base {"a":1,"\u0061":2}; source {}; target
{}; status invalid_input; side base; code duplicate_object_name.¶
number-overflow: base {}; source 1e309; target {}; status
invalid_input; side source; code number_out_of_binary64_range.¶
nested-decoded-duplicate-path: base
{"outer":{"a":1,"\u0061":2}}; source {}; target {}; status
invalid_input; side base; code duplicate_object_name; path /outer.¶
nested-number-overflow-path: base {}; source
{"outer":{"n":1e309}}; target {}; status invalid_input; side
source; code number_out_of_binary64_range; path /outer/n.¶
leading-bom: base is U+FEFF followed by {}; source {}; target {};
status invalid_input; side base; code invalid_json; path "".¶
lone-surrogate-escape: base "\uD800"; source {}; target {}; status
invalid_input; side base; code invalid_json; path "".¶
An array can represent a sequence, set, keyed collection, priority list, ordered log, tuple, or application-specific structure. A generic processor cannot infer whether index identity, element identity, order, or movement is semantically significant. Returning a typed unsupported result avoids standardizing an unsafe guess and leaves room for separately identified array profiles.¶
When a member is missing from base, there is no shared object at that path.
Recursively merging two divergent added objects against an invented empty base
would turn independent creation into implicit agreement. Reporting add_add
keeps that decision visible to application policy.¶
JSON permits insignificant whitespace, multiple number spellings, and unordered object members. This document fixes normalized values and diagnostic sequence order, which are the interoperability requirements for merge. It leaves byte canonicalization and content identity to specifications designed for those purposes.¶
The comparator for one decoded pointer token aligns with the property-name comparator defined by JCS. It also maps directly to string representations in ECMAScript, Java, and .NET. Runtimes that natively compare Unicode scalar values or UTF-8 bytes need an explicit UTF-16 conversion or equivalent code-unit iterator. This comparator alignment permits reuse of tested sorting routines, but complete paths are ordered as token sequences by this document and JCS remains unnecessary for merge processing.¶