Skip to content

feat: add render state schema generation#399

Draft
Markionium wants to merge 5 commits into
microsoft:mainfrom
Markionium:mapol/state-schema-tests
Draft

feat: add render state schema generation#399
Markionium wants to merge 5 commits into
microsoft:mainfrom
Markionium:mapol/state-schema-tests

Conversation

@Markionium

@Markionium Markionium commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Add webui schema to generate a render-state JSON Schema from an existing protocol.bin.
  • Add webui build --emit-schema to emit <protocol-stem>.state.schema.json directly from the in-memory build result.
  • Infer objects, arrays, scalar types, required and optional properties, component scopes, conditions, and integer predicates.
  • Generate a separate schema definition for each complete route chain.
  • Expose route-to-schema mappings through x-webui-routes.
  • Add non-validating x-webui.preferredType metadata for downstream type generation.
  • Add fixtures, snapshots, example-state validation, specifications, and CLI documentation.

Why

The original proof of concept combined schema inference and C# generation in one spike. It also flattened route branches into one state shape, losing the distinction between the state required by individual routes.

This PR separates the language-neutral schema contract so it can be reviewed and evolved independently.

For non-routed applications, the output is a normal object schema.

For routed applications, the output contains one definition per complete route chain:

{ 
  "$defs": {
    "route": {},
    "route.contacts.:id": {} 
  }, 
  "x-webui-routes": {
    "/": "#/$defs/route",
    "/contacts/:id": "#/$defs/route.contacts.:id"
  }
}

Route definitions remain independent even when two routes currently have the same state shape.

Schema inference

The schema is inferred from how compiled templates use state:

• Plain rendered values and normal attributes accept scalar values.
• Raw values infer strings.
• Dotted paths infer nested objects.
•  <for> collections infer arrays and item shapes.
• Condition-only paths are optional because missing condition state evaluates false.
• Boolean conditions infer booleans.
• Ordered comparisons infer numbers.
• Integer literals infer  integer .
• Known types propagate across path-to-path equality.
• Component bindings resolve back to parent state paths.
• Complex properties preserve structured values.
• Internal handler signals are excluded.

Broad validation types can include a generation hint without narrowing the JSON Schema contract:

{ 
  "type": ["string", "number", "boolean"], 
  "x-webui": {
    "preferredType": "string" 
  }
}

Why x-webui.preferredType exists

JSON Schema validation and DTO generation have slightly different goals.

The renderer is permissive. A normal text binding can render a string, number, or boolean:

{ 
  "type": ["string", "number", "boolean"]
}

However, generated DTOs should guide developers toward the type normally intended by the template:

  • Displayed values and normal attributes generally prefer string, because dates, counts, currency, and other values should usually be formatted before rendering.
  • Condition flags prefer  boolean .
  • Ordered numeric comparisons infer  number  or  integer  directly.
  • Structured or genuinely unknown values remain unconstrained.

Languages such as C# do not have a native string | number | boolean union. Generating  object  would provide little type safety and complicate System.Text.Json source generation.

The schema therefore keeps the complete runtime-valid type while adding a non-validating annotation:

{ 
  "type": ["string", "number", "boolean"], 
  "x-webui": {
    "preferredType": "string" 
  }
}

Standard JSON Schema validators use type and ignore x-webui. Host-language generators use preferredType to select a practical DTO type.

This keeps the schema accurate for dynamic consumers while producing useful strongly typed APIs for TypeScript, Rust, C#, and future generators.

Future: a WebUI JSON Schema vocabulary

x-webui is a pragmatic interim annotation. A better long-term design would be to publish a versioned WebUI render-state meta-schema and vocabulary built on JSON Schema Draft 2020-12.

That vocabulary could formally define:

  • Route-to-state mappings.
  • Preferred host-language types.
  • WebUI-specific generation metadata.
  • Allowed values and validation rules for those annotations.
  • Versioning and compatibility guarantees for external tooling.

Generated schemas could then reference a WebUI-specific $schema URI rather than only the generic Draft 2020-12 meta-schema. Standard JSON Schema behavior would remain available, while WebUI-aware validators and generators could also validate the additional metadata.

This PR does not define that vocabulary. The current x-webui shape is intentionally provisional so the schema and generation workflow can be evaluated first.

Validation

• Validate every repository example containing both  src/index.html  and  data/state.json .
• Snapshot component scope, nested collections, routed shells, and the complete Routes example.
• Cover nested and control-flow routes, optional and splat paths, nested arrays, complex properties,  .length , encoded route references, and literal-looking state keys.
•  cargo xtask check 
•  cd docs && pnpm build

Status and limitations

This remains work in progress:

  • Inference is based on template usage rather than an explicitly authored state declaration.
  • Route support is substantially improved over the original prototype, but may still have uncovered route/template combinations.
  • The schema is a build-time contract; runtime state is not currently validated against it.
  • x-webui is a provisional annotation. A versioned WebUI JSON Schema vocabulary and meta-schema would be preferable long term.
  • The implementation was largely AI-generated and still requires detailed human review before production use.

Host-language generation is intentionally excluded from this PR. A stacked follow-up demonstrates TypeScript, Rust, and C# generation on top of this schema contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fe040cbf-edcc-4e6f-b829-f85e08f2d044
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fe040cbf-edcc-4e6f-b829-f85e08f2d044
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fe040cbf-edcc-4e6f-b829-f85e08f2d044
Copilot AI review requested due to automatic review settings July 22, 2026 13:15
@Markionium
Markionium marked this pull request as draft July 22, 2026 13:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a render-state JSON Schema generation feature to WebUI, exposing it via a new webui schema command and an opt-in webui build --emit-schema output, along with fixture/snapshot-based coverage and documentation/spec updates.

Changes:

  • Add CLI support for emitting Draft 2020-12 JSON Schema for render state (webui schema and webui build --emit-schema).
  • Implement schema inference for state usage across bindings, control flow, collections, component scope, and routed route-chain bundles with x-webui-routes and x-webui.preferredType.
  • Add fixtures/snapshots plus example state validation, and update DESIGN + docs to describe the new contract and CLI flags.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
examples/app/contact-book-manager/data/state.json Adds missing state field used by templates for example validation.
examples/app/commerce/data/state.json Align example state keys/fields with inferred bindings and schema validation.
docs/guide/concepts/state-management/index.md Documents generating render-state schemas and related inference rules.
docs/guide/cli/index.md Documents webui schema and the --emit-schema build flag.
docs/ai.md Updates AI reference CLI flag table + schema command usage.
DESIGN.md Specifies the render-state JSON Schema contract and routed bundle shape.
crates/webui-cli/tests/fixtures/state-schema/routed-shell/settings-page.html New routed-shell fixture for route-chain schema inference.
crates/webui-cli/tests/fixtures/state-schema/routed-shell/index.html New routed-shell fixture entry.
crates/webui-cli/tests/fixtures/state-schema/routed-shell/home-page.html New routed-shell fixture with nested routes.
crates/webui-cli/tests/fixtures/state-schema/routed-shell/detail-page.html New routed-shell fixture leaf page.
crates/webui-cli/tests/fixtures/state-schema/routed-shell/app-shell.html New routed-shell fixture root shell with routes.
crates/webui-cli/tests/fixtures/state-schema/nested-collections/index.html New nested collections fixture entry.
crates/webui-cli/tests/fixtures/state-schema/nested-collections/group-list.html New nested collections fixture covering nested loops and .length.
crates/webui-cli/tests/fixtures/state-schema/component-scope/profile-card.html New component-scope fixture for attribute/local scope inference.
crates/webui-cli/tests/fixtures/state-schema/component-scope/nested-label.html New component-scope fixture for nested component lookups.
crates/webui-cli/tests/fixtures/state-schema/component-scope/index.html New component-scope fixture entry exercising bindings/conditions/raw.
crates/webui-cli/src/main.rs Wires the new Schema subcommand into the CLI entrypoint.
crates/webui-cli/src/commands/state_schema/tests.rs Adds schema inference snapshot tests + example-state/schema validation.
crates/webui-cli/src/commands/state_schema/snapshots/webui__commands__state_schema__tests__snapshots_routes_example_schema_bundle.snap Snapshot for routed schema bundle output for the Routes example.
crates/webui-cli/src/commands/state_schema/snapshots/webui__commands__state_schema__tests__snapshots_routes_declared_in_component_schema_bundle.snap Snapshot for route discovery in a component-defined routing shell.
crates/webui-cli/src/commands/state_schema/snapshots/webui__commands__state_schema__tests__snapshots_nested_collection_schema.snap Snapshot for nested collections and .length inference.
crates/webui-cli/src/commands/state_schema/snapshots/webui__commands__state_schema__tests__snapshots_component_scope_schema.snap Snapshot for component scope / attribute binding inference.
crates/webui-cli/src/commands/state_schema/scope.rs Implements scope resolution across root/component/loop bindings.
crates/webui-cli/src/commands/state_schema/model.rs Defines inference model + JSON Schema emission for inferred nodes.
crates/webui-cli/src/commands/state_schema/inference.rs Implements protocol walk and type/requiredness inference rules.
crates/webui-cli/src/commands/state_schema.rs Implements webui schema, route-chain bundling, and schema serialization.
crates/webui-cli/src/commands/mod.rs Exposes the new state_schema command module and clap variant.
crates/webui-cli/src/commands/build.rs Adds --emit-schema build output and filename collision checks.
crates/webui-cli/README.md Documents new CLI surface (--emit-schema, webui schema).
crates/webui-cli/Cargo.toml Adds dependencies needed for schema output ordering and snapshots.
Cargo.toml Adds workspace-level insta dependency for JSON snapshot testing.
Cargo.lock Locks new dependencies introduced by schema snapshot testing.

Comment on lines +419 to +427
while let Some((current_schema, current_value, path)) = pending.pop() {
let actual_type = json_type(current_value);
if !schema_accepts_type(current_schema, actual_type) {
errors.push(format!(
"{path}: expected {}, found {actual_type}",
display_schema_type(current_schema)
));
continue;
}
Comment on lines +456 to +462
fn schema_accepts_type(schema: &Value, actual: &str) -> bool {
match schema.get("type") {
Some(Value::String(expected)) => expected == actual,
Some(Value::Array(expected)) => expected.iter().any(|value| value.as_str() == Some(actual)),
_ => true,
}
}
Comment on lines +481 to +490
fn json_type(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fe040cbf-edcc-4e6f-b829-f85e08f2d044
Copilot AI review requested due to automatic review settings July 22, 2026 13:23
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: fe040cbf-edcc-4e6f-b829-f85e08f2d044

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (1)

crates/webui-cli/src/commands/state_schema/tests.rs:462

  • The schema validation helper treats schemas without a top-level type as accepting any value, which makes anyOf and boolean schemas (true/false) effectively unchecked. That can let example_state_files_match_generated_schemas pass even when a state violates an anyOf constraint (e.g. .length unions) or when a subschema is false.
fn schema_accepts_type(schema: &Value, actual: &str) -> bool {
    match schema.get("type") {
        Some(Value::String(expected)) => expected == actual,
        Some(Value::Array(expected)) => expected.iter().any(|value| value.as_str() == Some(actual)),
        _ => true,
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants