API Schema UI

Open /schema in any .NET 8+ ServiceStack App and you'll find a searchable index of every API the signed-in user can call. Open one and you get a working UI for it - form, validation, request preview, curl command, execution and response - with no code written and nothing installed.

The page isn't special. It fetches one small JSON document and hands it to a generic component:

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ApiFormSchema } from '@servicestack/vue'

const schema = ref()
onMounted(async () =>
    schema.value = await fetch('/schema/CreateCoffeeShopOrder.json').then(r => r.json()))
</script>

<template>
  <ApiFormSchema v-if="schema" :schema="schema" />
</template>

That's the whole integration - for that API, and every other one. The component never learns anything about CreateCoffeeShopOrder; the fetched schema supplies the fields, controls, validation, HTTP method and execution URL.

It's also what lets AI Chat render a trustworthy approval form for any API a Model proposes calling, without anyone building a Chat component per Request DTO.

Requirements

API Schemas are generated by the Metadata feature, so the only requirements are:

  • .NET 8+ ServiceStack App
  • MetadataFeature enabled (registered by default)

There's no separate schema plugin, no frontend project and no build step. Every API included in metadata and authorized for the current request appears automatically.

Routes

Route Response
{HTTP Method} /api/{RequestDto} Execute - send the typed Request DTO and receive its response
GET /schema Searchable HTML catalog of APIs available to the caller
GET /schema.json That catalog as JSON
GET /schema/{RequestDto} The contract, served as a complete executable HTML workbench
GET /schema/{RequestDto}.json The portable JSON Schema contract a generic UI or tool needs

/api/{RequestDto} executes the API; /schema/{RequestDto}.json explains how to use it. For example, GET /schema/QueryBookings.json describes how to invoke GET /api/QueryBookings; it does not execute it. The schema carries the API's fields, nested types, validation, authorization requirements, HTTP method and an $id pointing back at its execution URL.

The .json suffix is a convenience for clients and tools - the routes use ordinary HTTP content negotiation, so an Accept: application/json request to /schema/{RequestDto} returns the same document.

The API Schema (/schema) and AutoQuery Schema (/auto) route sets can each be disabled independently, see Configuration.

Browse every API at /schema

The built-in API browser is a fast searchable launcher designed for large applications. APIs can be filtered by Request DTO name, title, description, tag and HTTP verb, with fuzzy matching that understands the PascalCase names used by Request DTOs.

APIs excluded from metadata or unavailable to the caller are omitted, so signing in can change the catalog when additional role- or permission-protected APIs become available.

Because the page is generated at runtime it always reflects the application that's actually deployed, which makes it useful throughout an App's lifecycle:

  • Developers can explore an unfamiliar codebase without hunting for Service classes.
  • Frontend developers can invoke APIs before their production UI exists.
  • Testers can reproduce requests and validation errors without writing a client.
  • Support teams can use approved operational APIs directly.
  • API designers can see immediately whether their descriptions and validation produce a clear experience.
  • AI developers can inspect the exact schemas exposed to Models and approval forms.

What's in a schema

The schema is based on JSON Schema Draft-07, but describes more than the structural shape of a DTO - it contains enough to construct and invoke the API without loading ServiceStack's full metadata document.

Key Description
$schema The JSON Schema dialect
$id The stable pre-defined API route, e.g. /api/CreateCoffeeShopOrder
request The Request DTO name
operation Which AutoQuery/CRUD operation this is, e.g. Query, Create, Update, Patch, Delete, Save
method The HTTP method to call it with
title From [Api], [Description], or the humanized DTO name
description From [Notes] or [Description]
type, properties The object shape, including nested objects and collections
required Required fields
ui Control hints, help text, placeholders, layouts, lookups, uploads and formatting
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "/api/CreateCoffeeShopOrder",
  "request": "CreateCoffeeShopOrder",
  "method": "POST",
  "title": "Submit a Coffee Shop Order",
  "description": "Submits a validated coffee shop order",
  "type": "object",
  "required": ["CustomerName", "Items"],
  "properties": {
    "CustomerName": {
      "type": "string",
      "title": "Name to put on the order"
    },
    "Items": {
      "type": "array",
      "title": "Final order items",
      "items": {
        "type": "object",
        "properties": {
          "ProductId": { "type": "integer", "minimum": 1 },
          "Quantity": { "type": "integer", "minimum": 1 }
        }
      }
    }
  },
  "ui": {
    "submitLabel": "Create Coffee Shop Order"
  }
}

Constraints

Standard JSON Schema constraints are included when the equivalent information is present in ServiceStack metadata:

Constraint Typical source
minimum, maximum, exclusiveMinimum, exclusiveMaximum [Range], [ValidateGreaterThan], [ValidateLessThan] and related validators
minLength, maxLength [ValidateLength], [StringLength]
pattern [ValidateRegularExpression]
format Property type, e.g. date, date-time, email, uri
enum Enum types and [ApiAllowableValues]
minItems, maxItems Collection validators
required Non-nullable properties and [ValidateNotEmpty] / [ValidateNotNull]

Nested DTOs and collection item types are embedded within the same document, so a schema is self-contained: a renderer never has to resolve a type from elsewhere in the App's metadata.

The ui object

Everything a renderer needs that isn't part of JSON Schema's vocabulary lives under ui, including the control to use for each property, its placeholder and help text, form layout, submit label, reference lookups, file upload targets and display formatting.

ui is advisory. A renderer that ignores it still produces a correct - if plainer - form from the JSON Schema alone, which is what makes the same document usable by non-Vue clients, terminal tools and AI Assistants.

Authorization requirements

Schemas carry the API's authentication requirements so a UI can explain why an operation is unavailable:

  • requiresAuth
  • requiresApiKey
  • requiredRoles and requiresAnyRole
  • requiredPermissions and requiresAnyPermission
  • requiredClaims
  • requiredScopes

See Authorization remains server-owned for how these relate to enforcement.

INFO

$id is deliberately the pre-defined route (/api/{RequestDto}) rather than a custom [Route]. The pre-defined route always exists, never changes, and accepts every property in the body or query string - so it doubles as the URL to call. A custom [Route] can put properties in the path, which a generic client would have to reassemble.

Built to scale beyond thousands of APIs

ServiceStack's API Explorer at /ui loads the full MetadataApp document - every operation, DTO, data model and related type - before it can show you one API. That's convenient at smaller scales and increasingly expensive at larger ones; Apps with thousands of APIs could hit JavaScript engine limits during initialization.

API Schemas invert that dependency:

Opening an API loads only the schema required to render and invoke that operation. Nested DTOs the form needs are encapsulated within that schema, whilst unrelated APIs and models never enter the page.

  • Smaller payloads - one focused contract instead of the entire metadata graph.
  • Lower parsing and memory costs - the browser only materializes types visible in the current UI.
  • No giant JavaScript arguments - schemas are fetched and parsed as ordinary JSON.
  • Faster first use - search a compact catalog and open one API without waiting for every definition.
  • Independent caching - individual schemas can be cached and invalidated separately.
  • Bounded UI complexity - rendering cost is set by the selected API, not the App's total size.

An application can grow from ten APIs to ten thousand without making a single API form ten thousand times heavier.

Encapsulation

Because everything needed for the interaction travels with the selected API, a schema-powered component needs no global knowledge of the App and no privileged metadata singleton. In practice that means:

  • A page can render CreateBooking without also loading payroll, reporting and administration APIs.
  • A plugin can own its APIs and forms without coupling to the host App's complete metadata shape.
  • A micro-frontend can request only the capabilities within its own boundary.
  • APIs excluded from metadata or unavailable to the caller are never shipped to the client to render an unrelated form.

This is the same progressive disclosure model used by API Tools:

For traditional UIs this protects browser resources. For AI it protects the Model's finite context window.

Existing metadata becomes a richer UI

ServiceStack derives the document from the same metadata used by API Explorer and generated clients:

ServiceStack metadata Schema output
[Api], [Description], [Notes] API and field titles/descriptions
Property types and nullability JSON Schema types and required
Validation attributes and validators Structural constraints and validation hints
[Input] Widget, layout, placeholder, help and allowable values
Enums Selectable values and labels
[Ref], [References] Reference lookup metadata
[Intl], [Format] Display formatting
Auth attributes Authentication, role, permission, claim and scope requirements

So API Schemas reward the metadata already present in well-designed ServiceStack APIs:

[Tag("CoffeeShop")]
[Description("Submits a validated coffee shop order")]
[Route("/coffee-shop/orders", "POST")]
public class CreateCoffeeShopOrder : IPost, IReturn<CreateCoffeeShopOrderResponse>
{
    [Description("Name to put on the order")]
    [ValidateNotEmpty]
    public string CustomerName { get; set; } = "";

    [Description("Optional instructions applying to the whole order")]
    [Input(Type = "textarea", Placeholder = "e.g. call when ready")]
    public string? Notes { get; set; }

    [Description("Final order items")]
    [ValidateNotEmpty]
    public List<OrderItemRequest> Items { get; set; } = [];
}

The generated form uses the descriptions as labels and help text, marks required values, renders Notes as a textarea and turns Items into an editable nested form.

Other metadata unlocks richer controls:

  • Enums and [ApiAllowableValues] become constrained selections.
  • [ValidateGreaterThan], [ValidateLength], [Range] and related validators become client constraints.
  • [Input] and [Field] select widgets, placeholders, help text, steps and layout.
  • [Ref], [References] and foreign keys become searchable lookup UIs.
  • [UploadTo] becomes a file input with accepted file types.
  • [Intl] and [Format] describe how values should be displayed.
  • [FieldCss] and API form layouts control responsive presentation.
  • [Authenticate], roles, permissions, claims and scopes describe who can invoke the API.

None of this is specific to the schema page - it continues to improve ServiceStack's other Auto UIs, API Explorer and generated clients.

A complete API workbench

The UI at /schema/{RequestDto} is more than a generated form. As values are entered it shows the exact request that will be sent, and can switch to a copyable curl command to reproduce it from a terminal. It provides:

  • A generated request form
  • Client-side presentation of validation constraints
  • Nested object, collection, enum, date, upload and lookup controls
  • A request preview and copyable curl command
  • API execution with Data, JSON and Headers views of the response
  • Server validation errors mapped back to fields

How the request is sent

Submitting uses the method and $id from the schema:

API shape Request
GET, DELETE Values encoded in the query string
POST, PUT, PATCH JSON request body
Any API with file inputs multipart/form-data

The response panel shows status, size and duration. ServiceStack validation errors are bound back to their corresponding fields, whilst non-field errors remain in the form summary.

Inspecting the response

Each response can be viewed 3 different ways, switchable from the tabs in the response header:

  • Data - results rendered in a formatted results grid with nested complex types expanded inline
  • JSON - the syntax-highlighted raw JSON response
  • Headers - the HTTP response headers returned by the server

Responses can also be maximized to inspect larger results, expanding the response panel over the request form whilst retaining the same Data / JSON / Headers views.

The page remains a normal client of the API. All authentication, authorization, request filters and validation still run on the server when the request is submitted.

Shareable, executable queries

Query-string values pre-populate the form, making API examples shareable as ordinary links:

For GET APIs, opening a populated link can execute the request immediately. After submission the URL is updated with non-empty values, producing a durable, reloadable API query.

Powered by reusable Vue and React components

Rapid application development

Build API forms from schemas in minutes

Pass /schema/{Request}.json to ApiFormSchema to generate inputs, execute the API and inspect its response. Use JsonSchemaForm for standalone structured forms.

Generated inputs
Request and response
Auth and validation

The built-in pages aren't a separate UI framework - they're composed from the same components published in @servicestack/vue and @servicestack/react.

Component Purpose
ApiFormSchema The generic executable API form
ApiExplorerSchema The full workbench the built-in /schema/{RequestDto} page is built from
JsonSchemaForm Lower-level renderer for arbitrary JSON Schema values
JsonView Semantic rendering of a JSON response

npm install @servicestack/vue

npm install @servicestack/react

ApiFormSchema

ApiFormSchema renders only the form, leaving the surrounding workbench to the host page:

Prop Description
schema The /schema/{RequestDto}.json document to render
v-model / value The current request value
client JsonServiceClient to invoke the API with, e.g. for authenticated calls
auto-execute Execute GET APIs on load
sync-url Keep current values in the address bar

Everything it derives - the HTTP request preview, its curl equivalent, the request, the result and any error - is passed to its default slot, and completed calls emit success and error events:

Slot value Description
requestText The HTTP request that will be sent
curl The equivalent curl command
result Completed response with status, ms, size, text and json
error ServiceStack ResponseStatus when the call failed
loading Whether a request is in flight
<ApiFormSchema :schema="schema" v-model="request">
  <template #default="{ requestText, curl, result, error, loading }">
    <pre>{‎{ requestText }‎}</pre>
    <pre>{‎{ curl }‎}</pre>
    <pre v-if="result">{‎{ result.status }‎} · {‎{ result.ms }‎}ms · {‎{ result.size }‎}</pre>
    <pre v-if="result">{‎{ result.text }‎}</pre>
  </template>
</ApiFormSchema>

A host page can render the full workbench, only the curl command, or just the form - the component computes the same values regardless of which are displayed. To render the complete built-in workbench instead, use ApiExplorerSchema, which composes ApiFormSchema with the Request, Schema and Response panels.

The same component in React

The ApiFormSchema snippet above translates directly to React, where the default slot becomes a render prop:

import { useEffect, useMemo, useState } from 'react'
import { JsonServiceClient } from '@servicestack/client'
import { ApiFormSchema, JsonView } from '@servicestack/react'

export default function HelloForm() {
  const client = useMemo(() => new JsonServiceClient('/'), [])
  const [schema, setSchema] = useState<any>()

  useEffect(() => {
    fetch('/schema/Hello.json', { credentials: 'include' })
      .then(r => r.json())
      .then(setSchema)
  }, [])

  if (!schema) return null

  return (
    <ApiFormSchema schema={schema} client={client} value={‎{ Name: 'React' }‎}>
      {({ result, requestText, curl }) => (
        <section>
          <pre>{requestText}</pre>
          <details>
            <summary>curl</summary>
            <pre>{curl}</pre>
          </details>
          {result && <JsonView value={result.json ?? result.text} />}
        </section>
      )}
    </ApiFormSchema>
  )
}

Render any complex JSON Schema

JsonSchemaForm isn't limited to ServiceStack Request DTOs. It can recursively render any supported JSON Schema into a complete form, including nested objects, editable arrays, enums, dates, validation constraints and arbitrary dictionary properties. Its v-model binding keeps the full JSON value synchronized as the form changes.

The live Vue demo below combines all of these structures in one schema:

This makes schemas useful beyond executable API forms: the same renderer can power configuration editors, workflow inputs, structured content tools and any UI that needs to safely capture a complex JSON value without building a bespoke form for every shape.

Use JsonSchemaForm for arbitrary JSON values, or ApiFormSchema when the schema also describes an API that should be invoked. Both are available from @servicestack/vue and @servicestack/react; see Vue JSON Schema and the React Schema gallery for live examples.

The same schema and value can also generate C#, Python, TypeScript or JavaScript models with generateTypes.

One schema can describe any UI

The most valuable property of this design is that the schema isn't tied to the built-in page. It's a portable description of an interaction, and any renderer can map the same structure and UI hints into its own native controls:

  • A web form can use text fields, selectors, date pickers and nested panels.
  • A mobile App can render platform-native inputs.
  • A desktop administration tool can build property editors.
  • A terminal client can ask interactive questions.
  • A workflow engine can generate configuration steps.
  • A test tool can synthesize valid requests and boundary cases.
  • An AI Assistant can render a human approval form for a proposed tool call.

Approval UIs in AI Chat

Because the renderer is generic, AI Chat creates a Request preview and Approval UI on demand for any API it can discover. When a Model proposes a write operation the arguments are rendered with the same schema components used by the standalone API UI, so the user sees an editable form instead of an opaque JSON blob.

The schema returned by the api_describe API Tool is the same document served from /schema/{RequestDto}.json, so new APIs gain the same preview and approval capability as soon as their schema is available - there's no per-DTO Chat component to write.

Authorization remains server-owned

Schema discovery respects the current request and authenticated session. APIs excluded from metadata or inaccessible to the caller are not presented as available capabilities.

The authorization requirements listed in What's in a schema help UIs explain why an operation is unavailable, but they do not replace enforcement. The API remains the final authorization boundary: authorization is checked again when /api/{RequestDto} is called, so presentation adapts to the caller whilst security stays deterministic and server-side.

API Schema and /auto

An API Schema describes one Request DTO. An AutoQuery Schema groups the model schema with all authorized Query, Create, Update, Patch, Delete and Save API Schemas needed to build a CRUD UI.

Surface Scope
/schema/{RequestDto}.json One API operation
/auto/{ModelName}.json One data model and its authorized AutoQuery operations

Both are served by the Metadata feature and both respect the current session, so an App can use either or both.

Configuration

The schema routes belong to the Metadata feature, and each set can be disabled independently:

services.ConfigurePlugin<MetadataFeature>(feature => {
    // Don't register /schema and /schema/{RequestDto}
    feature.DisableApiSchema = true;
    // Don't register /auto and /auto/{DataModel}
    feature.DisableAutoQuerySchema = true;
});
Property Description
DisableApiSchema Don't register /schema and /schema/{RequestDto}
DisableAutoQuerySchema Don't register /auto and /auto/{DataModel}
IsApiSchemaEnabled Whether the API Schema routes are registered
IsAutoQuerySchemaEnabled Whether the AutoQuery Schema routes are registered
OnApiSchema Callback to modify each generated API Schema
OnAutoQuerySchema Callback to modify each generated AutoQuery Schema

An App can therefore keep its API workbench whilst withholding the AutoQuery data UIs, or the reverse. Disabling the HTML pages doesn't affect the JSON contract used by AI Chat and your own components.

Customizing generated schemas

Schemas can be augmented before they're returned. Because there's only one schema, a change applies everywhere it's used - the JSON contract, the built-in UI, your own components and any AI approval form rendered from it:

services.ConfigurePlugin<MetadataFeature>(feature => {
    feature.OnApiSchema = (requestType, schema) => {
        if (requestType == typeof(CreateCoffeeShopOrder))
            schema["ui"]!["submitLabel"] = "Place Order";
    };
});

OnAutoQuerySchema does the same for AutoQuery Schemas.

Prefer declarative DTO metadata when the change belongs to the API contract. Use OnApiSchema for application-wide conventions or cases that cannot be expressed with attributes.

Excluding APIs

An API is only described if it's included in metadata and authorized for the current request, so the existing mechanisms for restricting metadata also restrict schemas - [ExcludeMetadata], [Restrict] and the Metadata feature's own filters. There's no separate schema-level exclusion list to maintain.

Get Started

There's nothing to install - API Schemas are part of the Metadata feature, so any .NET 8+ ServiceStack App already serves them. Run your App and open:

The fastest way to see the value is to open /schema on an App you already have and search for an API you wrote months ago. Whatever descriptions and validation you gave it then are the UI you get now.