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
MetadataFeatureenabled (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:
requiresAuthrequiresApiKeyrequiredRolesandrequiresAnyRolerequiredPermissionsandrequiresAnyPermissionrequiredClaimsrequiredScopes
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
CreateBookingwithout 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
curlcommand - 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​
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.
@servicestack/vue
Generate executable API forms in Vue.
@servicestack/react
Generate the same forms in React, Vite or Next.js.
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.
Related​
- AutoQuery CRUD UI - the model-level equivalent for AutoQuery CRUD APIs
- Vue JSON Schema - live
ApiFormSchema,AutoQuerySchemaandJsonSchemaFormexamples - API Explorer - the full metadata-driven API UI at
/ui - API Tools - how AI Models discover and call these same APIs
- Metadata page - ServiceStack API metadata configuration