GhJSON Specification

JSON-based format for Grasshopper definitions

View the Project on GitHub architects-toolkit/ghjson-spec

GhPatch Format Specification

Version: 1.0 Status: Draft Last Updated: 2026-05-13


Table of Contents

  1. Introduction
  2. Document Structure
  3. Identity Rules
  4. Operations
  5. Apply Semantics
  6. Example
  7. Relationship to JSON Patch

1. Introduction

1.1 Purpose

GhPatch is a JSON-based patch format for GhJSON documents. A patch describes a set of add/remove/modify operations on components, connections, groups, and metadata at the GhJSON-semantic level — not at the JSON-pointer level.

GhPatch enables:

1.2 Design principles

  1. Semantic identity, not paths. Components, connections, and groups are identified by their GhJSON identity (instanceGuid, id), not by array index or JSON pointer. Reordering the components array does not invalidate a patch.
  2. No move operations. Component/connection/group order in the GhJSON arrays has no semantic meaning, so patches never need move.
  3. Structured grammar. Each entity has a small, fixed grammar (add / remove / modify with nested set / remove / sub-grammars), all derived from the GhJSON schema.
  4. Conflicts at apply time, not in the patch. A patch is a pure description of intent; the apply step is responsible for surfacing conflicts.
  5. Schema-versioned. A patch is tied to a specific GhJSON schema version. Cross-version patching is out of scope for 1.x.

1.3 File extension

GhPatch files use the .ghpatch extension.

1.4 Encoding

GhPatch files MUST be encoded in UTF-8 without BOM.

1.5 Relationship to GhJSON

GhPatch is a sibling format to GhJSON, not a profile of it. A .ghjson document is never also a .ghpatch document and vice versa. The two have separate JSON Schemas:


2. Document Structure

A GhPatch document is a JSON object with the following top-level structure:

{
  "schema": "1.0",
  "kind": "ghpatch",
  "patch": {
    "base": { ... },
    "metadata": { ... },
    "components": { ... },
    "connections": { ... },
    "groups": { ... }
  }
}

2.1 Top-level properties

Property Type Required Description
schema string No GhJSON schema version this patch targets (default: "1.0").
kind string Yes Discriminator. MUST be "ghpatch".
patch object Yes Patch body — see §4.

2.2 The patch.base reference

patch.base is an optional reference to the base document the patch was generated against:

{
  "base": {
    "schema": "1.0",
    "checksum": "sha256-abc123..."
  }
}
Property Type Description
schema string Schema version of the base document.
checksum string Content checksum of the normalised base document. Format: <algorithm>-<value>. The recommended algorithm is sha256.

The normalised form for checksum purposes:

  1. Apply the default Fix operations (assign missing IDs, regenerate metadata counters, etc.) without regenerating instance GUIDs.
  2. Drop volatile fields (metadata.modified, metadata.componentCount, metadata.connectionCount, metadata.groupCount, components[].warnings, components[].errors, components[].remarks).
  3. Sort each array deterministically (components by id, connections by (from.id, to.id, from.paramName, to.paramName), groups by id).
  4. Serialise as canonical JSON (sorted object keys, no insignificant whitespace).
  5. Hash with the chosen algorithm.

When patch.base.checksum is present, implementations MUST default to verifying it before applying and MUST refuse to apply on mismatch unless explicitly opted out (see §5).


3. Identity Rules

3.1 Component identity

When a patch operation references an existing component (via a match block or a remove entry), the implementation MUST resolve identity using this precedence:

  1. instanceGuid — match by instanceGuid if present in the match block AND in the base document.
  2. id — match by integer id.
  3. Structural fingerprint — match by { componentGuid, name } plus, when provided, pivot as a tie-breaker.

If no match block is provided that satisfies at least one of these rules, the operation MUST be reported as a conflict.

If multiple base components match a single match block, the operation MUST be reported as a conflict (the patch is ambiguous).

3.2 Connection identity

Connections are identified by their full (from, to) endpoint pair. Within an endpoint, paramName is the canonical parameter identifier; paramIndex is a fall-back when names are localised or have been renamed.

When diffing, implementations SHOULD canonicalise every endpoint to use paramName (resolving paramIndex against the component’s parameter list if needed). Patches emitted by diff tools SHOULD always include paramName.

When a patch references a connection that does not exist on the base (for remove) or that already exists (for add), the operation MUST be reported as a conflict.

3.3 Group identity

Groups are identified by instanceGuid (preferred) or id. Same precedence rules and conflict behaviour as components.

3.4 New-component ID handling

For components.add, the new component’s id MAY collide with an existing component on the base. When that happens:

instanceGuid collisions in components.add MUST be reported as a conflict.


4. Operations

4.1 patch.metadata

"metadata": {
  "set": { "title": "Updated", "author": "Jane" },
  "remove": ["description"]
}
Field Type Description
set object Metadata fields to set or replace. Same shape as documentMetadata in ghjson.schema.json.
remove string[] Metadata field names to remove.

set and remove MUST NOT both target the same field.

4.2 patch.components

"components": {
  "add":    [ <full component object> ],
  "remove": [ { "instanceGuid": "..." } ],
  "modify": [ { "match": { ... }, "set": { ... }, ... } ]
}

4.2.1 add

Each entry is a full component object as defined in ghjson.schema.json#/$defs/componentData. Identity fields (id, instanceGuid) SHOULD be present.

4.2.2 remove

Each entry is a componentMatch (see §3.1).

4.2.3 modify

Each entry has the shape:

{
  "match": { ... },
  "set":   { "pivot": "120,200", "nickName": "Add!" },
  "remove": ["warnings"],
  "componentState": {
    "set":    { "locked": true },
    "extensions": {
      "set":    { "gh.numberslider": { "value": "7<0~10>" } },
      "remove": ["gh.scribble"]
    }
  },
  "inputSettings": {
    "byParameterName": {
      "x": { "set": { "typeHint": "double" } }
    }
  },
  "outputSettings": { ... }
}

Implementations MAY support adding/removing entire entries to inputSettings / outputSettings in a future revision, but 1.0 keeps the surface area small and assumes parameters are not added/removed by a patch (which would imply a different component).

4.3 patch.connections

"connections": {
  "add":    [ { "from": { "id": 1, "paramName": "Number" }, "to": { "id": 2, "paramName": "A" } } ],
  "remove": [ { "from": { "id": 1, "paramName": "Number" }, "to": { "id": 2, "paramName": "B" } } ]
}

Connections are immutable in this format: there is no modify. To change a connection, remove it and add the new one.

4.4 patch.groups

"groups": {
  "add":    [ <full group object> ],
  "remove": [ { "instanceGuid": "..." } ],
  "modify": [
    {
      "match": { "id": 1 },
      "set":   { "name": "Inputs", "color": "argb:255,200,220,255" },
      "remove": [],
      "members": { "add": [3, 4], "remove": [9] }
    }
  ]
}

members.add / members.remove operate on the group’s members integer-id list. Members SHOULD reference components that exist on the base (or are being added in the same patch); references that resolve to neither SHOULD be reported as conflicts and dropped from the resulting document.


5. Apply Semantics

5.1 Order of operations

Patch application MUST proceed in this deterministic order:

  1. Verify patch.base.checksum if present (see §5.2).
  2. Apply patch.metadata.
  3. Apply patch.components.modify.
  4. Apply patch.components.remove.
  5. Apply patch.components.add (allocating new IDs as needed — §3.4).
  6. Apply patch.groups.* (modify → remove → add).
  7. Apply patch.connections.remove.
  8. Apply patch.connections.add.
  9. Run a final structural fix-up: drop connections that now reference missing components, drop group members that no longer exist, refresh metadata counters.

This ordering ensures that:

5.2 Base verification

When patch.base.checksum is present, the default behaviour is strict: refuse to apply when the checksum of the normalised base document does not match. Implementations MAY expose an “apply anyway” opt-out, but it MUST NOT be the default.

When patch.base is absent or has no checksum, no verification is performed.

5.3 Conflicts

A conflict is any situation where the patch cannot be applied unambiguously:

Kind Example
match_not_found A modify/remove references a component that is not on the base.
match_ambiguous A match block matches more than one base component.
instance_guid_collision A components.add has the same instanceGuid as an existing component.
connection_already_present A connections.add duplicates an existing connection.
connection_not_found A connections.remove targets a connection that does not exist.
dangling_member A groups.modify.members.add references a component that does not exist.
base_checksum_mismatch Base verification failed.

Implementations SHOULD report conflicts as a structured list. The default policy SHOULD be “apply what can be applied and report the rest”; “fail fast on first conflict” and “skip and report” SHOULD also be supported.

5.4 Idempotence

Applying the same patch twice on the same base document SHOULD produce:


6. Example

The following patch, applied to a base document containing a Number Slider (id 1) and an Addition component (id 2), would:

{
  "schema": "1.0",
  "kind": "ghpatch",
  "patch": {
    "base": {
      "schema": "1.0",
      "checksum": "sha256-abcd1234..."
    },
    "components": {
      "add": [
        {
          "name": "Panel",
          "instanceGuid": "33333333-3333-3333-3333-333333333333",
          "id": 3,
          "pivot": "500,100"
        }
      ],
      "modify": [
        {
          "match": { "instanceGuid": "11111111-1111-1111-1111-111111111111" },
          "componentState": {
            "extensions": {
              "set": {
                "gh.numberslider": { "value": "7<0~10>" }
              }
            }
          }
        },
        {
          "match": { "id": 2 },
          "set": { "nickName": "Add!" },
          "componentState": { "set": { "locked": true } }
        }
      ]
    },
    "connections": {
      "add": [
        { "from": { "id": 2, "paramName": "Result" }, "to": { "id": 3, "paramName": "Input" } }
      ]
    }
  }
}

7. Relationship to JSON Patch

GhPatch is not RFC 6902 JSON Patch. The two solve overlapping problems but at different levels:

Aspect JSON Patch (RFC 6902) GhPatch
Identity JSON Pointer (/components/3) GhJSON identity (instanceGuid, id, …)
Move support Yes (move op) No (array order is non-semantic)
Extension awareness None — pure JSON tree edits Aware of componentState.extensions opaque values
Apply order Sequential per-op Deterministic phase order (§5.1)
Conflicts Single failure (“test” op) Structured conflict list

JSON Patch is appropriate when treating a GhJSON document as a generic JSON tree (for example, when piping through generic JSON tooling). GhPatch is appropriate when treating it as a GhJSON document with first-class semantics.