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
| Field | Meaning |
|---|---|
Source | true = the port emits data (output). Input ports omit it (false) |
Position | Side of the node: module.Left, module.Right, module.Top, module.Bottom |
Configuration | Go value whose type is reflected into the port schema |
ResponseConfiguration | Go value describing the synchronous response shape (request/response ports) |
Schema | Raw 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 Type | JSON 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
| Tag | Description | Example |
|---|---|---|
json | JSON field name | json:"fieldName" |
title | Display label | title:"Field Name" |
description | Help text | description:"Enter value" |
default | Default value | default:"value" |
required | Required field | required:"true" |
enum | Allowed values | enum:"a,b,c" |
enumTitles | Display names | enumTitles:"A,B,C" |
format | Format / widget hint | format:"email", format:"textarea" |
language | Code editor language (with format:"code") | language:"json" |
readonly | Read-only field | readonly:"true" |
tab | Form tab grouping | tab:"Advanced" |
colSpan | Form grid width | colSpan:"col-span-6" |
requiredWhen / optionalWhen | Conditional required-ness | requiredWhen:"..." |
configurable | Per-edge editable definition | configurable:"true" |
shared | Definition shared across the flow | shared:"true" |
minimum / maximum | Number bounds | minimum:"0" |
minLength / maxLength | String length bounds | minLength:"1" |
minItems / maxItems | Array length bounds | minItems:"1" |
pattern | Regex pattern | pattern:"^[a-z]+$" |
Next Steps
- Secrets in Settings - Reference secrets
- Dynamic Schemas - Runtime schemas
- JSON Schema Basics - Schema fundamentals