Schema from Go

The TinySystems SDK generates JSON Schemas from Go structs. This makes defining port data structures simple and type-safe.

How Ports Get Schemas

For a typed port you normally don't call any schema function at all. Set Port.Configuration to a value of your Go type and leave Port.Schema nil — the runtime reflects the Configuration into a schema when it publishes the node's status:

import (
    "github.com/tiny-systems/module/module"
)

type Request struct {
    Method string `json:"method"`
    URL    string `json:"url"`
}

func (c *Component) Ports() []module.Port {
    return []module.Port{
        {
            Name:          "request",
            Label:         "Request",
            Position:      module.Left,
            Configuration: Request{}, // schema reflected automatically
        },
    }
}

schema.FromGo — the explicit escape hatch

schema.FromGo builds the same schema but hands you the bytes, as a json.RawMessage ready to assign to Port.Schema:

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

sch := schema.FromGo(Request{}) // json.RawMessage; nil on error

Use it only when you have a Go type for the shape you want to publish but still need the raw bytes — for example, picking one of several control shapes at runtime:

{Name: v1alpha1.ControlPort, Source: true, Schema: schema.FromGo(c.getControl())}

FromGo returns nil on error, which makes the runtime fall back to reflecting Configuration rather than publishing a broken schema.

When NOT to use it: for any port whose shape is its Go type. Set Configuration and leave Schema nil — that is the default path, and varying the shape by returning a different Go type per state also works through Configuration alone (see Dynamic Schemas).

Under the hood both paths call schema.CreateSchema, which returns a swaggest jsonschema.Schema; FromGo marshals it to bytes.

Port Fields

FieldMeaning
Sourcetrue = the port emits data (output). Input ports omit it (false)
PositionSide of the node: module.Left, module.Right, module.Top, module.Bottom
ConfigurationGo value whose type is reflected into the port schema
ResponseConfigurationGo value describing the synchronous response shape (request/response ports)
SchemaRaw json.RawMessage published verbatim instead of reflecting Configuration

By convention inputs sit on the left, outputs on the right, error outputs on the bottom, and system/control ports on top.

Struct Tags

JSON Tag

The json tag defines the field name in JSON:

type User struct {
    FirstName string `json:"firstName"`  // camelCase in JSON
    LastName  string `json:"lastName"`
    Email     string `json:"email"`
}

Title and Description

type Settings struct {
    Timeout int `json:"timeout" title:"Timeout (ms)" description:"Request timeout in milliseconds"`
    Retries int `json:"retries" title:"Retry Count" description:"Number of retry attempts"`
}

Type Mappings

Basic Types

Go TypeJSON Schema Type
string"type": "string"
int, int32, int64"type": "integer"
float32, float64"type": "number"
bool"type": "boolean"
[]T"type": "array"
struct"type": "object"
any, interface{}No type constraint (bare definition)
map[string]T"type": "object" with additionalProperties

Examples

type AllTypes struct {
    Text     string             `json:"text"`     // string
    Count    int                `json:"count"`    // integer
    Amount   float64            `json:"amount"`   // number
    Enabled  bool               `json:"enabled"`  // boolean
    Items    []string           `json:"items"`    // array of strings
    Metadata map[string]string  `json:"metadata"` // object with string values
    Data     any                `json:"data"`     // any type
}

Objects, arrays, any fields, and anything tagged configurable/shared are lifted into $defs entries and referenced with $ref — that is what the configurable overlay operates on.

Validation Tags

String Validation

type Input struct {
    Name    string `json:"name" minLength:"1" maxLength:"100"`
    Email   string `json:"email" format:"email"`
    Pattern string `json:"pattern" pattern:"^[a-z]+$"`
}

Number Validation

type Config struct {
    Port     int     `json:"port" minimum:"1" maximum:"65535"`
    Timeout  float64 `json:"timeout" minimum:"0.1" maximum:"30"`
    Attempts int     `json:"attempts" minimum:"1" maximum:"10"`
}

Array Validation

type Batch struct {
    Items []Item `json:"items" minItems:"1" maxItems:"100"`
}

Enum Values

Simple Enum

type Request struct {
    Method string `json:"method" enum:"GET,POST,PUT,DELETE"`
}

Enum with Titles

type Config struct {
    Level string `json:"level" enum:"debug,info,warn,error" enumTitles:"Debug,Info,Warning,Error"`
}

Default Values

type Settings struct {
    Host    string `json:"host" default:"localhost"`
    Port    int    `json:"port" default:"8080"`
    Timeout int    `json:"timeout" default:"30000"`
    Enabled bool   `json:"enabled" default:"true"`
}

Required Fields

type User struct {
    ID    string `json:"id" required:"true"`
    Name  string `json:"name" required:"true"`
    Email string `json:"email,omitempty"` // Optional
}

Nested Structures

type ServerConfig struct {
    Host string `json:"host" title:"Server Host" default:"localhost"`
    Port int    `json:"port" title:"Server Port" default:"8080"`
}

type AuthConfig struct {
    Username string `json:"username" title:"Username" required:"true"`
    Password string `json:"password" title:"Password" format:"password" required:"true"`
}

type Settings struct {
    Server ServerConfig `json:"server" title:"Server Configuration"`
    Auth   AuthConfig   `json:"auth" title:"Authentication"`
}

Pointer Types

Pointer fields become nullable in the generated schema; non-pointer fields have null stripped from their type:

type Message struct {
    ID    string `json:"id"`    // string
    Data  *Data  `json:"data"`  // object or null
    Count *int   `json:"count"` // integer or null
}

Maps

type Config struct {
    Headers    map[string]string `json:"headers" title:"HTTP Headers"`
    Properties map[string]any    `json:"properties" title:"Custom Properties"`
}

Format Tags

type Input struct {
    Email    string `json:"email" format:"email"`
    Website  string `json:"website" format:"uri"`
    Date     string `json:"date" format:"date"`
    DateTime string `json:"dateTime" format:"date-time"`
    UUID     string `json:"uuid" format:"uuid"`
    Password string `json:"password" format:"password"`
    Notes    string `json:"notes" format:"textarea"`
    Script   string `json:"script" format:"code" language:"javascript"`
}

Property Order

On reflected schemas the SDK stamps propertyOrder automatically from struct declaration order — a manual propertyOrder tag is overwritten. To change display order, reorder the fields. Explicit propertyOrder matters only in hand-written schemas assigned to Port.Schema (see Dynamic Schemas).

Complete Example

package httpclient

import (
    "github.com/tiny-systems/module/api/v1alpha1"
    "github.com/tiny-systems/module/module"
)

type HTTPRequest struct {
    Method  string            `json:"method" title:"HTTP Method" enum:"GET,POST,PUT,DELETE" default:"GET"`
    URL     string            `json:"url" title:"URL" format:"uri" required:"true"`
    Headers map[string]string `json:"headers,omitempty" title:"Headers"`
    Body    any               `json:"body,omitempty" title:"Request Body" configurable:"true"`
    Timeout int               `json:"timeout" title:"Timeout (ms)" default:"30000" minimum:"100" maximum:"300000"`
}

type HTTPResponse struct {
    StatusCode int               `json:"statusCode" title:"Status Code"`
    Headers    map[string]string `json:"headers" title:"Response Headers"`
    Body       any               `json:"body" title:"Response Body"`
}

type ErrorOutput struct {
    Error   string      `json:"error" title:"Error Message"`
    Request HTTPRequest `json:"request" title:"Original Request"`
}

func (c *HTTPClient) Ports() []module.Port {
    return []module.Port{
        {
            Name:          v1alpha1.SettingsPort,
            Label:         "Settings",
            Configuration: c.settings,
        },
        {
            // Input: receives the request to execute
            Name:          "request",
            Label:         "Request",
            Position:      module.Left,
            Configuration: HTTPRequest{},
        },
        {
            // Output: emits the response downstream
            Name:          "response",
            Label:         "Response",
            Source:        true,
            Position:      module.Right,
            Configuration: HTTPResponse{},
        },
        {
            // Output: emits failures
            Name:          "error",
            Label:         "Error",
            Source:        true,
            Position:      module.Bottom,
            Configuration: ErrorOutput{},
        },
    }
}

Tag Reference

TagDescriptionExample
jsonJSON field namejson:"fieldName"
titleDisplay labeltitle:"Field Name"
descriptionHelp textdescription:"Enter value"
defaultDefault valuedefault:"value"
requiredRequired fieldrequired:"true"
enumAllowed valuesenum:"a,b,c"
enumTitlesDisplay namesenumTitles:"A,B,C"
formatFormat / widget hintformat:"email", format:"textarea"
languageCode editor language (with format:"code")language:"json"
readonlyRead-only fieldreadonly:"true"
tabForm tab groupingtab:"Advanced"
colSpanForm grid widthcolSpan:"col-span-6"
requiredWhen / optionalWhenConditional required-nessrequiredWhen:"..."
configurablePer-edge editable definitionconfigurable:"true"
sharedDefinition shared across the flowshared:"true"
minimum / maximumNumber boundsminimum:"0"
minLength / maxLengthString length boundsminLength:"1"
minItems / maxItemsArray length boundsminItems:"1"
patternRegex patternpattern:"^[a-z]+$"

Next Steps