Skip to content

Configurable Schema Overlay

When nodes are connected by edges, the platform derives the edge schema from the target port's native schema and overlays configurable definitions from both source and target nodes. This page documents the SDK functions involved in that process.

How Edge Schemas Are Built

Target Port Native Schema (from Status)


GetConfigurableDefinitions(targetNode)  ──┐
GetConfigurableDefinitions(sourceNode)  ──┤
         │                                │
         ▼                                │
MergeConfigurableDefinitions(target, source)


UpdateWithDefinitions(nativeSchema, mergedDefs)


     Edge Schema (sent to UI)
  1. The native schema comes from the target port's Status (runtime-generated by the component's Ports() method).
  2. Configurable definitions are collected from both target and source nodes' Spec ports — these are $defs entries marked configurable: true.
  3. Definitions are merged with priority rules to prevent bare definitions from destroying rich ones.
  4. The merged definitions are overlaid onto the native schema, replacing matching $defs entries.

GetConfigurableDefinitions

go
import "github.com/tiny-systems/module/pkg/utils"

defs := utils.GetConfigurableDefinitions(node v1alpha1.TinyNode, from *string) map[string]*ajson.Node

Collects all configurable $defs entries from a node:

  1. Status ports — collects definitions with shared: true (runtime schema).
  2. Spec ports — collects definitions with configurable: true (user-configured schema, typically from _settings).

The from parameter filters Spec ports to only those matching a specific source port name. Pass nil to collect from all Spec ports.

Example:

go
// Collect target node's defs, filtered to edges from this specific source port
targetDefs := utils.GetConfigurableDefinitions(targetNode, &sourcePortFullName)

// Collect source node's defs (all Spec ports)
sourceDefs := utils.GetConfigurableDefinitions(sourceNode, nil)

MergeConfigurableDefinitions

go
import "github.com/tiny-systems/module/pkg/utils"

merged := utils.MergeConfigurableDefinitions(
    targetDefs map[string]*ajson.Node,
    sourceDefs map[string]*ajson.Node,
) map[string]*ajson.Node

Merges source node definitions into target node definitions. The merge protects rich target definitions from being overwritten by bare source definitions.

A definition is considered rich if it has explicit properties or an explicit type field. A definition is bare if it has neither (e.g., type Context any generates a bare definition with only additionalProperties).

Merge Rules

SourceTargetWinnerReason
barebaresourceBoth minimal, source is newer
barerichtargetRich target protected from bare overwrite
richbaresourceSource has more info
richrichsourceSource is newer, both have content

Why this matters: Without this protection, a router's bare Context (from type Context any) would overwrite a debug node's typed Context (type: string), causing the edge UI to show "any" instead of a proper string editor.

Example:

go
targetDefs := utils.GetConfigurableDefinitions(targetNode, &from)
sourceDefs := utils.GetConfigurableDefinitions(sourceNode, nil)

// Safe merge — bare source won't destroy rich target
utils.MergeConfigurableDefinitions(targetDefs, sourceDefs)

WARNING

The function modifies targetDefs in place and returns it. Do not use the original targetDefs map after calling this function — use the return value.

UpdateWithDefinitions

go
import "github.com/tiny-systems/module/pkg/schema"

updatedSchema, err := schema.UpdateWithDefinitions(
    nativeSchema []byte,
    configurableDefinitionNodes map[string]*ajson.Node,
) ([]byte, error)

Overlays configurable definitions onto a native schema. For each $defs entry in the native schema, if a matching configurable definition exists, it replaces the native definition while preserving:

  • path — the JSON path within the parent struct
  • configurable — whether the definition is configurable
  • shared / readonly — propagation flags

Matching is done by exact key name first, then by path fallback (handles cases where source and target use different type names for the same field, e.g., Context vs Startcontext).

The required field is stripped from overlays — configurable definitions carry required constraints from their originating node, but those don't apply at downstream edges where users freely map different data.

HasProperties

go
import "github.com/tiny-systems/module/pkg/schema"

rich := schema.HasProperties(n *ajson.Node) bool

Returns true if the JSON Schema definition node has a properties key with at least one child property. Returns false for bare definitions that only have additionalProperties.

Used internally by MergeConfigurableDefinitions and UpdateWithDefinitions to determine definition richness.

Examples:

json
// HasProperties = true
{"type": "object", "properties": {"name": {"type": "string"}}}

// HasProperties = false (bare — type Context any)
{"additionalProperties": {}}

// HasProperties = false (has type but no properties)
{"type": "string"}

Complete Example

A typical edge rendering pipeline (simplified from platform's buildGraphEvents):

go
// 1. Get the native schema from the target port's Status
nativeSchema := statusPortSchemaMap[edge.To]

// 2. Collect configurable definitions from both nodes
sourcePortFullName := utils.GetPortFullName(sourceNodeName, edge.Port)
targetDefs := utils.GetConfigurableDefinitions(targetNode, &sourcePortFullName)
sourceDefs := utils.GetConfigurableDefinitions(sourceNode, nil)

// 3. Merge — protects rich target defs from bare source overwrites
utils.MergeConfigurableDefinitions(targetDefs, sourceDefs)

// 4. Overlay merged definitions onto native schema
edgeSchema, err := schema.UpdateWithDefinitions(nativeSchema, targetDefs)

Next Steps

Build flow-based applications on Kubernetes