Skip to content

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. sourceDescriptions required when any operation declares sourceDescription)
  • The operation three-shape oneOf
  • unevaluatedProperties: false on 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 (loop requires items, await requires wait, etc.)
  • StructuralResult.from linkage and kind/type match
  • dependsOn cycle 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 numeric wait/batchSize literals, root-scoped terminal goto, and revised forEach merge 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 an Extensions field, verifies that struct JSON tags exactly match its knownFields list. A mismatch means the unmarshaller would reject valid documents or silently accept invalid ones.
  • TestSchemaParity_KnownFieldsMatchSchema — compares each type's knownFields against the corresponding $def in versions/1.13.0.json. Drift in either direction fails the build.
  • TestSchemaParity_DefCoverageIsExhaustive — fails when versions/1.13.0.json grows a $def that no parity entry tracks. Tripwire for adding a new type without wiring it through the extension machinery.
  • TestSchemaConformance_* — reads versions/1.13.0.json and asserts that every required, enum, pattern, and propertyNames rule 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.


← Extension Profiles | Next: Interchange Formats →