JSON-based format for Grasshopper definitions
Version: 1.0 Status: Draft Last Updated: 2026-05-13
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:
.ghjson files (like git diff).instanceGuid, id), not by array index or JSON pointer. Reordering the components array does not invalidate a patch.move.add / remove / modify with nested set / remove / sub-grammars), all derived from the GhJSON schema.1.x.GhPatch files use the .ghpatch extension.
GhPatch files MUST be encoded in UTF-8 without BOM.
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:
https://architects-toolkit.github.io/ghjson-spec/schema/v1.0/ghjson.schema.jsonhttps://architects-toolkit.github.io/ghjson-spec/schema/v1.0/ghpatch.schema.jsonA GhPatch document is a JSON object with the following top-level structure:
{
"schema": "1.0",
"kind": "ghpatch",
"patch": {
"base": { ... },
"metadata": { ... },
"components": { ... },
"connections": { ... },
"groups": { ... }
}
}
| 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. |
patch.base referencepatch.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:
Fix operations (assign missing IDs, regenerate metadata counters, etc.) without regenerating instance GUIDs.metadata.modified, metadata.componentCount, metadata.connectionCount, metadata.groupCount, components[].warnings, components[].errors, components[].remarks).components by id, connections by (from.id, to.id, from.paramName, to.paramName), groups by id).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).
When a patch operation references an existing component (via a match block or a remove entry), the implementation MUST resolve identity using this precedence:
instanceGuid — match by instanceGuid if present in the match block AND in the base document.id — match by integer id.{ 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).
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.
Groups are identified by instanceGuid (preferred) or id. Same precedence rules and conflict behaviour as components.
For components.add, the new component’s id MAY collide with an existing component on the base. When that happens:
id for the new component.id (e.g. a connections.add from a freshly-added component) MUST be rewritten to use the allocated id.instanceGuid collisions in components.add MUST be reported as a conflict.
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.
patch.components"components": {
"add": [ <full component object> ],
"remove": [ { "instanceGuid": "..." } ],
"modify": [ { "match": { ... }, "set": { ... }, ... } ]
}
addEach entry is a full component object as defined in ghjson.schema.json#/$defs/componentData. Identity fields (id, instanceGuid) SHOULD be present.
removeEach entry is a componentMatch (see §3.1).
modifyEach 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": { ... }
}
match — componentMatch (required).set / remove — operate on top-level scalar component fields (name, library, nickName, componentGuid, instanceGuid, id, pivot).componentState — operations on componentState. The nested extensions sub-grammar treats each extension value (gh.panel, gh.csharp, …) as opaque: set replaces the whole extension object, remove deletes it.inputSettings / outputSettings — operations on the parameter settings list, keyed by parameterName. Each per-parameter op supports set and remove of fields inside that parameterSettings entry.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).
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.
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.
Patch application MUST proceed in this deterministic order:
patch.base.checksum if present (see §5.2).patch.metadata.patch.components.modify.patch.components.remove.patch.components.add (allocating new IDs as needed — §3.4).patch.groups.* (modify → remove → add).patch.connections.remove.patch.connections.add.This ordering ensures that:
modify against a component that is later removed is reported as a conflict, not silently lost.connections.add can reference a component freshly created in components.add.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.
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.
Applying the same patch twice on the same base document SHOULD produce:
The following patch, applied to a base document containing a Number Slider (id 1) and an Addition component (id 2), would:
5 to 7.Add!.{
"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" } }
]
}
}
}
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.