Validation Patterns Tutorial#

Learn how to validate resolver outputs, enforce constraints on inputs, and use schema validation to catch issues early.

Overview#

scafctl offers multiple validation layers that work together to ensure configuration correctness:

block-beta
  columns 4
  block:top:4
    A["Solution YAML"]
  end
  B["Lint<br/>Rules"] C["Schema<br/>Helper Tags"] D["Validate<br/>Provider"] E["Result<br/>Schema"]
  block:bottom:4
    F["Runtime Validation (CEL + Regex)"]
  end
  • Schema helper tags — provider-level input constraints (type, length, pattern, enum)
  • Validation provider — runtime regex and CEL-based checks on resolver values
  • Result schemas — JSON Schema validation of action outputs
  • Lint rules — static analysis at scafctl lint time

1. Schema-Level Validation with Provider Inputs#

Provider schemas use JSON Schema constraints to reject invalid input before execution.

Available Constraints#

ConstraintApplies ToHelper FunctionDescription
enumstrings, intsWithEnum(vals...)Restrict to allowed values
patternstringsWithPattern(regex)Must match regex
minLength / maxLengthstringsWithMinLength(n) / WithMaxLength(n)String length bounds
minimum / maximumnumbersWithMinimum(n) / WithMaximum(n)Numeric range bounds
minItems / maxItemsarraysWithMinItems(n) / WithMaxItems(n)Array size bounds
formatstringsWithFormat(fmt)Semantic format hint (uri, email, date, uuid)
defaultanyWithDefault(val)Default value if omitted

Example: Provider Schema in Go#

schema := schemahelper.ObjectSchema(
    []string{"name", "environment"}, // required fields
    map[string]*jsonschema.Schema{
        "name": schemahelper.StringProp("Service name",
            schemahelper.WithPattern(`^[a-z][a-z0-9-]*$`),
            schemahelper.WithMinLength(3),
            schemahelper.WithMaxLength(63),
            schemahelper.WithExample("my-service"),
        ),
        "environment": schemahelper.StringProp("Target environment",
            schemahelper.WithEnum("development", "staging", "production"),
            schemahelper.WithExample("production"),
        ),
        "replicas": schemahelper.IntProp("Number of replicas",
            schemahelper.WithMinimum(1),
            schemahelper.WithMaximum(100),
            schemahelper.WithDefault(3),
        ),
        "tags": schemahelper.ArrayProp("Resource tags",
            schemahelper.WithMaxItems(20),
            schemahelper.WithItems(schemahelper.StringProp("tag value",
                schemahelper.WithMaxLength(128),
            )),
        ),
        "callback_url": schemahelper.StringProp("Webhook callback URL",
            schemahelper.WithFormat("uri"),
        ),
    },
)

When a resolver passes invalid input to this provider, the lint system catches it at analysis time:

scafctl lint my-solution.yaml
scafctl lint my-solution.yaml
ERROR [invalid-provider-input-type] resolver 'deploy': input 'replicas' expects integer, got string
ERROR [schema-violation] resolver 'deploy': input 'name' does not match pattern '^[a-z][a-z0-9-]*$'

2. Runtime Validation with the validation Provider#

The validation provider runs checks during the validate phase of a resolver, or as a standalone transform.

Pattern: Regex Matching#

Validate string values against regex patterns:

# examples/providers/validation/regex-patterns.yaml
apiVersion: scafctl.io/v1alpha1
kind: Solution
metadata:
  name: regex-validation-demo
spec:
  resolvers:
    - name: email
      value: "user@example.com"
      validate:
        with:
          - provider: validation
            input:
              match: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
              message: "Must be a valid email address"

    - name: semver
      value: "v1.2.3"
      validate:
        with:
          - provider: validation
            input:
              match: '^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$'
              message: "Must be a valid semantic version"

    - name: k8s_name
      value: "my-service-01"
      validate:
        with:
          - provider: validation
            input:
              match: '^[a-z][a-z0-9-]{0,61}[a-z0-9]$'
              notMatch: '--'
              message: "Must be a valid Kubernetes resource name"
▶ Try it

Run this example

scafctl run solution -f regex-patterns.yaml

Pattern: CEL Expression Validation#

Use CEL expressions for complex, type-aware validation:

# examples/providers/validation/cel-validation.yaml
apiVersion: scafctl.io/v1alpha1
kind: Solution
metadata:
  name: cel-validation-demo
spec:
  resolvers:
    - name: port
      value: 8080
      validate:
        with:
          - provider: validation
            input:
              expression: "__self >= 1024 && __self <= 65535"
              message: "Port must be in the unprivileged range (1024-65535)"

    - name: environment
      value: "staging"
      validate:
        with:
          - provider: validation
            input:
              expression: "__self in ['development', 'staging', 'production']"
              message: "Environment must be one of: development, staging, production"

    - name: password
      value: "SecureP@ss1"
      validate:
        with:
          - provider: validation
            input:
              match: '.{8,}'
              message: "Password must be at least 8 characters"
          - provider: validation
            input:
              match: '[A-Z]'
              message: "Password must contain an uppercase letter"
          - provider: validation
            input:
              match: '[0-9]'
              message: "Password must contain a digit"
          - provider: validation
            input:
              match: '[!@#$%^&*]'
              message: "Password must contain a special character"

Tip: __self refers to the resolver’s current value inside validate and transform phases. Use _ to access the full resolver data map for cross-field validation.

Pattern: Cross-Field Validation#

Validate relationships between different resolver values:

apiVersion: scafctl.io/v1alpha1
kind: Solution
metadata:
  name: cross-field-validation
spec:
  resolvers:
    - name: min_replicas
      value: 2

    - name: max_replicas
      value: 10
      validate:
        with:
          - provider: validation
            input:
              expression: "__self >= _.min_replicas"
              message: "max_replicas must be >= min_replicas"

    - name: environment
      value: "production"

    - name: replicas
      value: 3
      validate:
        with:
          - provider: validation
            input:
              expression: |
                _.environment == 'production'
                  ? __self >= 3
                  : __self >= 1
              message: "Production requires at least 3 replicas"

3. Action Result Schema Validation#

Actions can define a resultSchema to validate their output structure using JSON Schema:

apiVersion: scafctl.io/v1alpha1
kind: Solution
metadata:
  name: result-schema-demo
spec:
  resolvers:
    - name: config
      resolve:
        with:
          - provider: http
            input:
              method: GET
              url: https://api.example.com/config

  actions:
    - name: deploy
      resultSchema:
        type: object
        required:
          - status
          - deploymentId
        properties:
          status:
            type: string
            enum: [success, pending, failed]
          deploymentId:
            type: string
            pattern: "^deploy-[a-f0-9]{8}$"
          instances:
            type: array
            minItems: 1
            items:
              type: object
              required: [id, region]
              properties:
                id:
                  type: string
                region:
                  type: string
                  enum: [us-east-1, us-west-2, eu-west-1]
        additionalProperties: false
      steps:
        - provider: exec
          input:
            command: "echo '{\"status\":\"success\",\"deploymentId\":\"deploy-a1b2c3d4\",\"instances\":[{\"id\":\"i-1\",\"region\":\"us-east-1\"}]}'"

If the action output doesn’t match the schema, execution fails with a clear error:

ERROR: action 'deploy' result does not match schema: property 'status' must be one of [success, pending, failed]

4. Lint-Time Static Validation#

scafctl lint catches issues without executing the solution:

scafctl lint my-solution.yaml
scafctl lint my-solution.yaml

Key Validation Rules#

RuleSeverityWhat It Catches
schema-violationerrorUnknown fields, type mismatches, pattern violations
unknown-provider-inputerrorInput key not in provider’s schema
invalid-provider-input-typeerrorLiteral value wrong type for field
invalid-expressionerrorCEL syntax errors
invalid-templateerrorGo template syntax errors
finally-with-foreacherrorforEach in finally block (unsupported)
resolver-self-referenceerrorUsing _.name instead of __self in validate/transform
permissive-result-schemainforesultSchema without type constraint
undefined-required-propertyerrorRequired property not defined in properties
# Lint with auto-fix where possible
scafctl lint --fix my-solution.yaml

# Lint all solutions in a directory
scafctl lint ./solutions/
# Lint with auto-fix where possible
scafctl lint --fix my-solution.yaml

# Lint all solutions in a directory
scafctl lint ./solutions/

5. Common Validation Patterns Reference#

String Patterns#

# Email
match: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

# URL (http/https)
match: '^https?://[^\s/$.?#].[^\s]*$'

# Semantic version
match: '^v?\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$'

# Kubernetes resource name (RFC 1123)
match: '^[a-z][a-z0-9-]{0,61}[a-z0-9]$'

# UUID
match: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'

# IPv4 address
match: '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$'

# CIDR notation
match: '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$'

# ISO 8601 date
match: '^\d{4}-\d{2}-\d{2}$'

Numeric Constraints with CEL#

# Port range
expression: "__self >= 1 && __self <= 65535"

# Unprivileged port
expression: "__self >= 1024 && __self <= 65535"

# Percentage
expression: "__self >= 0.0 && __self <= 100.0"

# Memory limit (MB, must be power of 2)
expression: "__self > 0 && __self % 2 == 0"

# Timeout (1s to 5min)
expression: "__self >= 1 && __self <= 300"

Collection Constraints with CEL#

# Non-empty array
expression: "size(__self) > 0"

# Array max size
expression: "size(__self) <= 50"

# All items match pattern
expression: "__self.all(item, item.matches('^[a-z-]+$'))"

# At least one item meets criteria
expression: "__self.exists(item, item.startsWith('prod-'))"

# No duplicates
expression: "size(__self) == size(__self.distinct())"

# Map has required keys
expression: "has(__self.region) && has(__self.zone)"

Multi-Rule Composition#

Apply multiple validation rules by chaining validation provider entries:

validate:
  with:
    - provider: validation
      input:
        match: '.{8,}'
        message: "Must be at least 8 characters"
    - provider: validation
      input:
        match: '[A-Z]'
        message: "Must contain an uppercase letter"
    - provider: validation
      input:
        notMatch: '\s'
        message: "Must not contain whitespace"
    - provider: validation
      input:
        expression: "__self != _.username"
        message: "Must not match the username"

6. Validation Failure Diagnostics#

When validation fails, scafctl provides structured error messages. Use the MCP explain_error tool or inspect the output directly:

# Run with verbose output to see validation details
scafctl run solution -f my-solution.yaml -v 2
# Run with verbose output to see validation details
scafctl run solution -f my-solution.yaml -v 2

Common validation error patterns and their meaning:

Error PatternCauseFix
validation failed: matchValue didn’t match the regex in matchCheck the regex or the input value
validation failed: notMatchValue matched the regex in notMatchEnsure value doesn’t contain forbidden pattern
validation failed: expressionCEL expression returned falseReview the expression and input data
'field' is required for X operationMissing required provider inputAdd the field to your resolver input
unknown operationInvalid operation valueCheck the provider’s enum list

Next Steps#