Feature 9: Validation
← Extension Profiles | Next: Interchange Formats →
github.com/OpenUdon/uws layers two kinds of validation. Running both catches errors at the contract boundary — before any HTTP call leaves the machine.
Two Layers
Layer 1: Structural (JSON Schema)
The published JSON Schema (versions/1.1.1.json) validates document shape:
- Required fields (
uws,info,operations) - Type and format constraints
- Enum values (workflow types, action types, criterion types)
- Pattern constraints (ID formats, output label formats)
- Conditional rules (e.g.
sourceDescriptionsrequired when any operation declaressourceDescription) - The operation three-shape
oneOf unevaluatedProperties: falseon every object type
Layer 2: Semantic (Go validator)
(*uws1.Document).Validate() and ValidateResult() catch rules the schema cannot express:
- Duplicate identifiers (operationId, workflowId, stepId, triggerId, result names)
- Reference integrity across operations, source descriptions, workflows, steps, trigger routes
- Operation binding mutual exclusivity
- Structural-type field constraints (
looprequiresitems,awaitrequireswait, etc.) StructuralResult.fromlinkage and kind/type matchdependsOncycle detection- Component variable key patterns
The Go API
Validate() — returns the first error as a single error, suitable for a quick pass:
if err := doc.Validate(); err != nil {
log.Fatal(err)
}
ValidateResult() — returns every error with a dotted path:
result := doc.ValidateResult()
if !result.Valid() {
for _, issue := range result.Errors {
fmt.Printf("%s: %s\n", issue.Path, issue.Message)
}
}
Example 1: Common Errors and Their Path-Tagged Output
A document with several mistakes at once:
uws: "1.0.0"
info:
title: Broken Workflow
version: 1.0.0
operations:
- operationId: fetch
sourceDescription: missing_api # sourceDescription not declared
openapiOperationId: getData
onFailure:
- name: bad_retry
type: retry
# retryLimit is missing
- operationId: fetch # duplicate operationId
x-uws-operation-profile: udon
ValidateResult() output:
sourceDescriptions: is required when any operation declares sourceDescription; operations[0].sourceDescription is "missing_api"
operations[0].sourceDescription: references unknown sourceDescription "missing_api"
operations[0].onFailure[0]: retry requires retryLimit > 0
operations[1].operationId: duplicate operationId "fetch"
Each error names the exact field and what is wrong — directly actionable.
Runtime supplement payloads have their own schema. If an operation uses
x-uws-operation-profile: uws.runtime.1.0, validate its x-uws-runtime object
against versions/runtime.1.0.json: the type field is required, only the
published non-HTTP runtime selectors are allowed, and HTTP/OpenAPI metadata is
not accepted there.
Example 2: Reference Integrity Errors
workflows:
- workflowId: main
type: sequence
steps:
- stepId: s1
operationRef: nonexistent_op # operation not declared
- stepId: s1 # duplicate stepId
workflow: ghost_workflow # workflow not declared
workflows[0].steps[0].operationRef: references unknown operationId "nonexistent_op"
workflows[0].steps[1].stepId: duplicate stepId "s1"
workflows[0].steps[1].workflow: references unknown workflowId "ghost_workflow"
Example 3: Dependency Cycle Detection
operations:
- operationId: a
sourceDescription: api
openapiOperationId: opA
dependsOn: [b]
- operationId: b
sourceDescription: api
openapiOperationId: opB
dependsOn: [a] # a depends on b, b depends on a → cycle
dependsOn: cycle detected: a -> b -> a
Complete Parse-and-Validate Example
package main
import (
"fmt"
"log"
"os"
"github.com/OpenUdon/uws/convert"
"github.com/OpenUdon/uws/uws1"
)
func main() {
data, err := os.ReadFile("workflow.uws.yaml")
if err != nil {
log.Fatal(err)
}
var doc uws1.Document
if err := convert.UnmarshalYAML(data, &doc); err != nil {
log.Fatalf("parse error: %v", err)
}
result := doc.ValidateResult()
if !result.Valid() {
fmt.Println("Validation failed:")
for _, issue := range result.Errors {
fmt.Printf(" %s: %s\n", issue.Path, issue.Message)
}
os.Exit(1)
}
fmt.Println("Document is valid.")
}
Example 4: AI Agent Corrective Loop
Path-tagged errors are structured enough to feed directly back to a language model:
for attempt := 0; attempt < 3; attempt++ {
proposedYAML := agent.ProduceWorkflow(userIntent)
var doc uws1.Document
if err := convert.UnmarshalYAML([]byte(proposedYAML), &doc); err != nil {
userIntent = fmt.Sprintf("Previous attempt had a parse error: %v\n\nOriginal request: %s", err, userIntent)
continue
}
result := doc.ValidateResult()
if result.Valid() {
doc.SetRuntime(rt)
return doc.Execute(ctx)
}
// Build corrective prompt from path-tagged errors
var errLines []string
for _, e := range result.Errors {
errLines = append(errLines, fmt.Sprintf("%s: %s", e.Path, e.Message))
}
userIntent = fmt.Sprintf(
"The workflow had validation errors. Fix them and try again:\n%s\n\nOriginal request: %s",
strings.Join(errLines, "\n"), userIntent,
)
}
Errors like operations[0].onFailure[0]: retry requires retryLimit > 0 give the model exactly what to fix without prose interpretation.
Schema/Code/Spec Sync
The three artifacts that define UWS are kept in sync by a reflection-driven test suite:
TestSchemaParity_StructTagsMatchKnownFields— for every Go struct with anExtensionsfield, verifies that struct JSON tags exactly match itsknownFieldslist. A mismatch means the unmarshaller would reject valid documents or silently accept invalid ones.TestSchemaParity_KnownFieldsMatchSchema— compares each type'sknownFieldsagainst the corresponding$definversions/1.1.1.json. Drift in either direction fails the build.TestSchemaParity_DefCoverageIsExhaustive— fails whenversions/1.1.1.jsongrows a$defthat no parity entry tracks. Tripwire for adding a new type without wiring it through the extension machinery.TestSchemaConformance_*— readsversions/1.1.1.jsonand asserts that everyrequired,enum, andpatternrule the schema declares is also covered by the Go validator.
Adding a property to one artifact without updating the others fails the build immediately.
From The Big Fixture
The large fixture is a regression fixture as much as an example. Its generator validates the source document and then validates every parsed interchange form:
var fromHCL uws1.Document
if err := convert.UnmarshalHCL(hclData, &fromHCL); err != nil {
return fmt.Errorf("unmarshal HCL: %w", err)
}
if err := fromHCL.Validate(); err != nil {
return fmt.Errorf("validate HCL document: %w", err)
}
if !bytes.Contains(hclRoundTrip, []byte(`"x-uws-runtime"`)) {
return fmt.Errorf("HCL round-trip lost x-uws-runtime payloads")
}
Full context: testdata/big/main.go.