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 JSON Schema selected by the document's exact uws value validates
document shape. The latest published core schema is
versions/1.13.0.json; it is not substituted for older or unpublished versions.
- 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
- Reference steps cannot declare structural child blocks, including empty arrays
- Structural-type field constraints (
looprequiresitems,awaitrequireswait, etc.) StructuralResult.fromlinkage and kind/type matchdependsOncycle detection- Canonical decimal array indexes in JSON Pointer criteria
- Direct and indirect recursive workflow-call detection
- Component variable key patterns
- From UWS 1.10, expression-addressable output, variable, workflow-input, and step-input names; executable identifier uniqueness and entry selection
- From UWS 1.11, response-body dot-walks, loop-only
$batchIndex, complete numericwait/batchSizeliterals, root-scoped terminalgoto, and revisedforEachmerge record selection - From UWS 1.12, operation effect enums and pending-step contracts; pending-only documents may omit placeholder operations, but executable validation rejects pending steps before runtime calls
- UWS 1.9.1 patch gating and identifier/output/input integrity for
contentTrust - UWS 1.10 through 1.12 features are gated by the declared version; earlier published versions retain their own expression, wait, and identifier rules
validation.ValidateDocumentFile and the browser-profile helpers enable the
built-in JSON Schema format assertions used by UWS contracts. The lower-level
generic validation.ValidateFile follows JSON Schema draft 2020-12 and leaves
format as an annotation unless the supplied schema requires the assertion
vocabulary.
Version and Schema Selection
Published UWS versions select the schema with the exact matching filename
(for example, uws: 1.9.1 selects versions/1.9.1.json). Schema lookup does
not infer a version from the newest available schema and does not silently
substitute another version. Core semantic validation and execution also require
the declared version to have a published core schema. A custom or externally
supplied schema does not make an unpublished version supported; a
prerelease, future version, or unpublished patch/minor version fails closed
even if a same-named file is available. The generic validation.ValidateFile
API may validate against any explicitly supplied schema, but it does not
establish that the document is a supported UWS core version.
Feature gates compare valid, supported UWS 1.x Semantic Versions using SemVer
precedence. Prerelease text must follow SemVer syntax; the published schema's
broad version pattern is not sufficient to make malformed syntax valid. A
syntactically valid prerelease such as 1.9.2-rc.1 is still unsupported
unless that exact core version is published. Missing uws remains invalid;
schema lookup's historical 1.0.0 default is only a bootstrap for structural
validation and does not make an undeclared version valid.
The exact-version admission guard is an intentional cross-version bug fix: documents declaring older published versions now fail closed when the declared version itself is unpublished. This guard does not apply later feature rules to older documents; those rules remain gated by their declared version.
Advisory Content-Trust Analysis
Content-trust analysis is a separate explicit pass:
report, err := contenttrust.Analyze(ctx, &doc, resolvers...)
Malformed declarations are validation errors. Analyzer findings are not: they
do not enter ValidationResult, prevent execution, or alter executor results.
The deterministic report contains paths, fixed messages, and provenance/
capability metadata without runtime values or content excerpts. Resolvers add
source- and extension-profile channel semantics; unresolved extension flow stays
opaque or unknown. See Content Trust.
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/API/event source
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.13.0.json. Drift in either direction fails the build.TestSchemaParity_DefCoverageIsExhaustive— fails whenversions/1.13.0.jsongrows a$defthat no parity entry tracks. Tripwire for adding a new type without wiring it through the extension machinery.TestSchemaConformance_*— readsversions/1.13.0.jsonand asserts that everyrequired,enum,pattern, andpropertyNamesrule 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.