Internet-Draft JSON Three-Way Merge August 2026
Jurkovikj Expires 5 February 2027 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-jurkovikj-json-three-way-merge-00
Published:
Intended Status:
Experimental
Expires:
Author:
A. Jurkovikj

Deterministic Three-Way Merge for JSON Values

Abstract

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.

Status of This Memo

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.

Table of Contents

1. Introduction

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.

1.1. Goals

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.

1.2. Non-Goals

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.

2. Relationship to Existing JSON Standards

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.

3. Conventions and Terminology

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.

Base

The shared JSON value from which source and target were independently derived.

Source

One edited JSON value derived from the base. The name does not grant this side authority over target.

Target

The other edited JSON value derived from the base. The name does not grant this side authority over source.

Normalized value

A value admitted by the input-domain rules (Section 4), with every number represented by its normalized finite binary64 value.

Present

An object member exists and has a JSON value, including JSON null.

Missing

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.

Resource-policy identifier

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.

Merge processor

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.

4. Input Domain and Normalization

4.1. One Complete JSON Value

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.

4.2. Binary64 Number Model

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.

4.3. Merge Value Kinds

The merge domain contains these normalized JSON value kinds:

Arrays are recursively unsupported. An array at the root or below an object member prevents that complete input from entering the merge engine.

4.4. Normalized Equality and Kind

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.

5. Validation and Failure Precedence

5.1. Input-Side Order

The merge processor MUST process complete inputs in this order:

  1. base;

  2. source; and

  3. target.

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.

5.2. Within One Input Side

Within one side, processing is bounded and left-to-right. The processor MUST use this precedence at each parser event:

  1. Check the raw input-octet ceiling before scanning.

  2. Before entering a recognized container, check nesting depth and node count.

  3. Before constructing a recognized scalar, check node count.

  4. When an object member begins, check member count before decoding and retaining its name; check decoded name size while decoding it.

  5. Detect a duplicate decoded member name before parsing that member's value.

  6. Before parsing an array child, check any per-array element ceiling and the document node ceiling.

  7. Check decoded string size and raw number-token size while scanning.

  8. Report syntax, Unicode, and numeric-domain failures at the first point they become knowable.

  9. 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.

5.3. Unsupported Arrays

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:

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.

5.4. Invalid Input

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.

5.5. Resource Policy

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:

Table 1
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).

6. Deterministic Merge Operation

6.1. Equality Shortcuts and Merge Laws

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).

6.2. Scalar and Type Merge

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.

6.3. Object Merge

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.

6.4. Object-Member Presence Matrix

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.

Table 2
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.

6.5. No Partial Result

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.

7. Conflicts and Diagnostic Ordering

7.1. Conflict Kinds

The conflict kinds are:

concurrent_value_change

Base, 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_change

Base, source, and target are present; no equality shortcut applies; and the three merge value kinds are not all the same.

add_add

The member is missing from base and both sides add unequal normalized values.

source_delete_target_edit

The member is present in base, source deletes it, and target changes it to a value unequal to base.

source_edit_target_delete

The 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.

7.2. Presence Encoding

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.

7.3. Path and Conflict Ordering

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.

8. JSON Result Form

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.

Table 3
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

8.1. Merged

{
  "status": "merged",
  "merged": {"name":"ready","owner":"B"}
}

The merged member MUST contain the complete normalized merged value.

8.2. Conflict

{
  "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).

8.3. Unsupported

{
  "status": "unsupported",
  "error": {
    "code": "arrays_not_supported",
    "side": "source",
    "path": "/items"
  }
}

8.4. Invalid Input

{
  "status": "invalid_input",
  "error": {
    "code": "duplicate_object_name",
    "side": "base",
    "path": ""
  }
}

8.5. Resource Limit

{
  "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:

Table 4
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.

9. Examples

9.1. Independent Object Changes

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"}

9.2. Missing Is Not Null

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.

9.3. Delete Versus Edit

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.

9.4. Divergent Additions Are Conservative

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.

9.5. Binary64 Equality

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}.

10. Informative HTTP Reconciliation Example

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]:

  1. base is the representation state previously observed by the client;

  2. source is the client's complete proposed state derived from base;

  3. target is freshly obtained current state;

  4. the application runs this merge operation;

  5. conflicts are reviewed or resolved under application policy; and

  6. 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.

11. Security Considerations

11.1. Resource Exhaustion

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.

11.2. Conflict-Value Disclosure

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.

11.3. Merge Success Is Not Semantic Safety

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.

11.4. Base Authenticity and Current State

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.

11.5. Binary64 Rounding

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.

11.6. Unicode and Path Presentation

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.

11.7. Unsupported Arrays

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.

12. IANA Considerations

This document has no IANA actions.

13. Implementation Status

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.

14. References

14.1. Normative References

[IEEE754]
IEEE, "IEEE Standard for Floating-Point Arithmetic", IEEE 754-2019, DOI 10.1109/IEEESTD.2019.8766229, , <https://ieeexplore.ieee.org/document/8766229>.
[RFC6901]
Bryan, P., Ed., Zyp, K., and M. Nottingham, Ed., "JavaScript Object Notation (JSON) Pointer", RFC 6901, DOI 10.17487/RFC6901, , <https://www.rfc-editor.org/rfc/rfc6901>.
[RFC8259]
Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/rfc/rfc8259>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.

14.2. Informative References

[RFC6902]
Bryan, P., Ed. and M. Nottingham, Ed., "JavaScript Object Notation (JSON) Patch", RFC 6902, DOI 10.17487/RFC6902, , <https://www.rfc-editor.org/rfc/rfc6902>.
[RFC7396]
Hoffman, P. and J. Snell, "JSON Merge Patch", RFC 7396, DOI 10.17487/RFC7396, , <https://www.rfc-editor.org/rfc/rfc7396>.
[RFC7493]
Bray, T., Ed., "The I-JSON Message Format", RFC 7493, DOI 10.17487/RFC7493, , <https://www.rfc-editor.org/rfc/rfc7493>.
[RFC8785]
Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, DOI 10.17487/RFC8785, , <https://www.rfc-editor.org/rfc/rfc8785>.
[RFC9110]
Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, , <https://www.rfc-editor.org/rfc/rfc9110>.
[I-D.jurkovikj-httpapi-agentic-state]
Jurkovikj, A., "HTTP Profile for Conditional Updates to Shared Resource State (Agentic State Transfer)", Work in Progress, Internet-Draft, draft-jurkovikj-httpapi-agentic-state-02, , <https://datatracker.ietf.org/doc/html/draft-jurkovikj-httpapi-agentic-state-02>.
[RFC7942]
Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/rfc/rfc7942>.

Appendix A. Merge Pseudocode

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

Appendix B. Conformance Vectors

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.

B.1. Successful Merge Vectors

  • 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.

B.2. Conflict Vectors

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.

B.3. Unsupported-Array Vectors

  • 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.

B.4. Invalid-Input Precedence Vectors

  • 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 "".

Appendix C. Design Rationale

C.1. Why Arrays Fail Closed

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.

C.2. Why Divergent Additions Do Not Recurse

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.

C.3. Why Merge Is Value-Deterministic Rather Than Byte-Canonical

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.

C.4. Why UTF-16 Code-Unit Ordering

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.

Author's Address

Antun Jurkovikj
North Macedonia