| description | Defining apcore module input/output schemas — Pydantic models, YAML JSON Schema Draft 2020-12, x-* LLM extension fields, runtime validation, and cross-language schema sharing. |
|---|
Schema is the core of apcore, defining the input and output structure of modules.
Traditional Module:
def process(data): # What is data? Unknown
return result # What is result? Unknown
apcore Module:
input_schema = ProcessInput # Input structure is clear
output_schema = ProcessOutput # Output structure is clear
def execute(inputs, context):
# inputs are validated, structure is known
return {...} # Output will be validated
Purpose of Schema:
| Purpose | Description |
|---|---|
| AI Understanding | LLM knows how to call the module through Schema |
| Auto Validation | Framework automatically validates input and output |
| Documentation Generation | Automatically generate API documentation |
| Type Safety | Type hints available during development |
| Cross-Language | YAML Schema can be shared across languages |
The preferred method when defining schemas directly in code. Each SDK uses the idiomatic schema construction approach for its language: Pydantic for Python, TypeBox for TypeScript, and serde_json::json! for Rust.
=== "Python"
```python
from pydantic import BaseModel, Field
from typing import Literal
class Address(BaseModel):
"""Address information"""
province: str = Field(..., description="Province")
city: str = Field(..., description="City")
district: str = Field(..., description="District")
detail: str = Field(..., description="Detailed address")
postal_code: str = Field(..., description="Postal code", pattern=r"^\d{6}$")
class OrderInput(BaseModel):
"""Order creation input"""
# Required fields
product_id: str = Field(
..., # ... means required
description="Product ID",
min_length=1,
max_length=50,
)
quantity: int = Field(
...,
description="Purchase quantity",
ge=1, # >= 1
le=100, # <= 100
)
# Optional fields (must have default value)
note: str | None = Field(
None, # Default value
description="Order note",
max_length=500,
)
# Enum type
payment_method: Literal["alipay", "wechat", "card"] = Field(
"alipay",
description="Payment method",
)
# Nested object
shipping_address: Address = Field(
...,
description="Shipping address",
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// Address sub-schema
const Address = Type.Object({
province: Type.String({ description: 'Province' }),
city: Type.String({ description: 'City' }),
district: Type.String({ description: 'District' }),
detail: Type.String({ description: 'Detailed address' }),
postal_code: Type.String({
description: 'Postal code',
pattern: '^\\d{6}$',
}),
});
// Order creation input
const OrderInput = Type.Object({
// Required fields
product_id: Type.String({
description: 'Product ID',
minLength: 1,
maxLength: 50,
}),
quantity: Type.Integer({
description: 'Purchase quantity',
minimum: 1,
maximum: 100,
}),
// Optional fields (must have default value)
note: Type.Optional(
Type.Union([Type.String({ maxLength: 500 }), Type.Null()], {
description: 'Order note',
default: null,
}),
),
// Enum type
payment_method: Type.Union(
[Type.Literal('alipay'), Type.Literal('wechat'), Type.Literal('card')],
{ description: 'Payment method', default: 'alipay' },
),
// Nested object
shipping_address: Type.Composite([Address], {
description: 'Shipping address',
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// Address sub-schema
fn address_schema() -> Value {
json!({
"type": "object",
"properties": {
"province": {"type": "string", "description": "Province"},
"city": {"type": "string", "description": "City"},
"district": {"type": "string", "description": "District"},
"detail": {"type": "string", "description": "Detailed address"},
"postal_code": {
"type": "string",
"description": "Postal code",
"pattern": "^\\d{6}$"
}
},
"required": ["province", "city", "district", "detail", "postal_code"]
})
}
// Order creation input
fn order_input_schema() -> Value {
json!({
"type": "object",
"properties": {
// Required fields
"product_id": {
"type": "string",
"description": "Product ID",
"minLength": 1,
"maxLength": 50
},
"quantity": {
"type": "integer",
"description": "Purchase quantity",
"minimum": 1,
"maximum": 100
},
// Optional fields (with default)
"note": {
"type": ["string", "null"],
"description": "Order note",
"maxLength": 500,
"default": null
},
// Enum type
"payment_method": {
"type": "string",
"description": "Payment method",
"enum": ["alipay", "wechat", "card"],
"default": "alipay"
},
// Nested object
"shipping_address": address_schema()
},
"required": ["product_id", "quantity", "shipping_address"]
})
}
```
Used for sharing Schema definitions across languages.
# schemas/executor/order/create_order.schema.yaml
$schema: "https://apcore.dev/schema/v1"
version: "1.0.0"
module_id: "executor.order.create_order"
description: |
Order creation module
Supports multiple payment methods and shipping address configuration.
input_schema:
type: object
properties:
product_id:
type: string
description: "Product ID"
minLength: 1
maxLength: 50
quantity:
type: integer
description: "Purchase quantity"
minimum: 1
maximum: 100
note:
type: string
description: "Order note"
maxLength: 500
default: null
payment_method:
type: string
description: "Payment method"
enum: ["alipay", "wechat", "card"]
default: "alipay"
shipping_address:
$ref: "#/definitions/Address"
description: "Shipping address"
required: [product_id, quantity, shipping_address]
output_schema:
type: object
properties:
order_id:
type: string
description: "Order ID"
status:
type: string
description: "Order status"
enum: ["created", "pending", "paid", "failed"]
total_amount:
type: number
description: "Total order amount"
created_at:
type: string
format: date-time
description: "Creation time"
required: [order_id, status, total_amount, created_at]
definitions:
Address:
type: object
properties:
province:
type: string
description: "Province"
city:
type: string
description: "City"
district:
type: string
description: "District"
detail:
type: string
description: "Detailed address"
postal_code:
type: string
description: "Postal code"
pattern: "^\\d{6}$"
required: [province, city, district, detail, postal_code]=== "Python"
```python
from pydantic import BaseModel, Field
from typing import Any
class BasicTypes(BaseModel):
# String
name: str = Field(..., description="Name")
# Integer
age: int = Field(..., description="Age")
# Float
price: float = Field(..., description="Price")
# Boolean
active: bool = Field(..., description="Is active")
# Any type (avoid when possible)
data: Any = Field(..., description="Any data")
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const BasicTypes = Type.Object({
// String
name: Type.String({ description: 'Name' }),
// Integer
age: Type.Integer({ description: 'Age' }),
// Float
price: Type.Number({ description: 'Price' }),
// Boolean
active: Type.Boolean({ description: 'Is active' }),
// Any type (avoid when possible)
data: Type.Unknown({ description: 'Any data' }),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn basic_types_schema() -> Value {
json!({
"type": "object",
"properties": {
// String
"name": {"type": "string", "description": "Name"},
// Integer
"age": {"type": "integer", "description": "Age"},
// Float
"price": {"type": "number", "description": "Price"},
// Boolean
"active": {"type": "boolean", "description": "Is active"},
// Any type (avoid when possible)
"data": {"description": "Any data"}
},
"required": ["name", "age", "price", "active", "data"]
})
}
```
Corresponding JSON Schema:
properties:
name:
type: string
age:
type: integer
price:
type: number
active:
type: boolean
data: {} # Any type=== "Python"
```python
from pydantic import BaseModel, Field
from typing import Optional
class OptionalTypes(BaseModel):
# Method 1: | None
email: str | None = Field(None, description="Email")
# Method 2: Optional (equivalent to above)
phone: Optional[str] = Field(None, description="Phone")
# Non-None field with default value
count: int = Field(default=0, description="Count")
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const OptionalTypes = Type.Object({
// Nullable optional
email: Type.Optional(
Type.Union([Type.String(), Type.Null()], { description: 'Email', default: null }),
),
// Same shape, alternate spelling
phone: Type.Optional(
Type.Union([Type.String(), Type.Null()], { description: 'Phone', default: null }),
),
// Non-null field with default value
count: Type.Integer({ description: 'Count', default: 0 }),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn optional_types_schema() -> Value {
json!({
"type": "object",
"properties": {
// Nullable optional
"email": {"type": ["string", "null"], "description": "Email", "default": null},
// Same shape, alternate spelling
"phone": {"type": ["string", "null"], "description": "Phone", "default": null},
// Non-null field with default value
"count": {"type": "integer", "description": "Count", "default": 0}
}
// No "required" array — all fields are optional
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field
from typing import Any
class OrderItem(BaseModel):
product_id: str = Field(..., description="Product ID")
quantity: int = Field(..., description="Quantity")
price: float = Field(..., description="Unit price")
class CollectionTypes(BaseModel):
# String list
tags: list[str] = Field(default=[], description="Tag list")
# Object list
items: list[OrderItem] = Field(..., description="Order items")
# Dictionary
metadata: dict[str, Any] = Field(default={}, description="Metadata")
# Dictionary with specified value type
scores: dict[str, int] = Field(default={}, description="Score mapping")
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const OrderItem = Type.Object({
product_id: Type.String({ description: 'Product ID' }),
quantity: Type.Integer({ description: 'Quantity' }),
price: Type.Number({ description: 'Unit price' }),
});
const CollectionTypes = Type.Object({
// String list
tags: Type.Array(Type.String(), { description: 'Tag list', default: [] }),
// Object list
items: Type.Array(OrderItem, { description: 'Order items' }),
// Dictionary
metadata: Type.Record(Type.String(), Type.Unknown(), {
description: 'Metadata',
default: {},
}),
// Dictionary with specified value type
scores: Type.Record(Type.String(), Type.Integer(), {
description: 'Score mapping',
default: {},
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn order_item_schema() -> Value {
json!({
"type": "object",
"properties": {
"product_id": {"type": "string", "description": "Product ID"},
"quantity": {"type": "integer", "description": "Quantity"},
"price": {"type": "number", "description": "Unit price"}
},
"required": ["product_id", "quantity", "price"]
})
}
fn collection_types_schema() -> Value {
json!({
"type": "object",
"properties": {
// String list
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tag list",
"default": []
},
// Object list
"items": {
"type": "array",
"items": order_item_schema(),
"description": "Order items"
},
// Dictionary
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Metadata",
"default": {}
},
// Dictionary with specified value type
"scores": {
"type": "object",
"additionalProperties": {"type": "integer"},
"description": "Score mapping",
"default": {}
}
},
"required": ["items"]
})
}
```
Corresponding JSON Schema:
properties:
tags:
type: array
items:
type: string
default: []
items:
type: array
items:
$ref: "#/definitions/OrderItem"
metadata:
type: object
additionalProperties: true
default: {}
scores:
type: object
additionalProperties:
type: integer
default: {}=== "Python"
```python
from pydantic import BaseModel, Field
from typing import Literal
from enum import Enum
# Method 1: Literal (recommended)
class Order1(BaseModel):
status: Literal["pending", "paid", "shipped", "done"] = Field(
...,
description="Order status",
)
priority: Literal[1, 2, 3] = Field(
default=2,
description="Priority: 1-high 2-medium 3-low",
)
# Method 2: Enum
class OrderStatus(str, Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DONE = "done"
class Order2(BaseModel):
status: OrderStatus = Field(..., description="Order status")
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// Method 1: Union of literal values (recommended)
const Order1 = Type.Object({
status: Type.Union(
[
Type.Literal('pending'),
Type.Literal('paid'),
Type.Literal('shipped'),
Type.Literal('done'),
],
{ description: 'Order status' },
),
priority: Type.Union(
[Type.Literal(1), Type.Literal(2), Type.Literal(3)],
{ description: 'Priority: 1-high 2-medium 3-low', default: 2 },
),
});
// Method 2: Reused enum constant
const OrderStatus = Type.Union(
[
Type.Literal('pending'),
Type.Literal('paid'),
Type.Literal('shipped'),
Type.Literal('done'),
],
{ $id: 'OrderStatus' },
);
const Order2 = Type.Object({
status: Type.Composite([OrderStatus], { description: 'Order status' }),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// Method 1: enum keyword on a string field (recommended)
fn order1_schema() -> Value {
json!({
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Order status",
"enum": ["pending", "paid", "shipped", "done"]
},
"priority": {
"type": "integer",
"description": "Priority: 1-high 2-medium 3-low",
"enum": [1, 2, 3],
"default": 2
}
},
"required": ["status"]
})
}
// Method 2: reused enum sub-schema
fn order_status_schema() -> Value {
json!({
"type": "string",
"enum": ["pending", "paid", "shipped", "done"]
})
}
fn order2_schema() -> Value {
let mut status = order_status_schema();
status["description"] = json!("Order status");
json!({
"type": "object",
"properties": { "status": status },
"required": ["status"]
})
}
```
=== "Python"
```python
from datetime import datetime, date, time
from pydantic import BaseModel, Field
class DateTimeTypes(BaseModel):
# Datetime
created_at: datetime = Field(..., description="Creation time")
# Date only
birth_date: date = Field(..., description="Birth date")
# Time only
alarm_time: time = Field(..., description="Alarm time")
# Date string format (needs to be parsed manually)
date_str: str = Field(
...,
description="Date string",
pattern=r"^\d{4}-\d{2}-\d{2}$", # YYYY-MM-DD
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const DateTimeTypes = Type.Object({
// Datetime (ISO 8601)
created_at: Type.String({
description: 'Creation time',
format: 'date-time',
}),
// Date only
birth_date: Type.String({
description: 'Birth date',
format: 'date',
}),
// Time only
alarm_time: Type.String({
description: 'Alarm time',
format: 'time',
}),
// Date string format (custom pattern)
date_str: Type.String({
description: 'Date string',
pattern: '^\\d{4}-\\d{2}-\\d{2}$', // YYYY-MM-DD
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn datetime_types_schema() -> Value {
json!({
"type": "object",
"properties": {
// Datetime (ISO 8601)
"created_at": {
"type": "string",
"description": "Creation time",
"format": "date-time"
},
// Date only
"birth_date": {
"type": "string",
"description": "Birth date",
"format": "date"
},
// Time only
"alarm_time": {
"type": "string",
"description": "Alarm time",
"format": "time"
},
// Date string format (custom pattern)
"date_str": {
"type": "string",
"description": "Date string",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$"
}
},
"required": ["created_at", "birth_date", "alarm_time", "date_str"]
})
}
```
!!! note "The format: rows are aspirational"
No SDK maps a format to a native type today — apcore-python annotates a format: date-time field as str, not datetime. format is an annotation, not an assertion (TYPE_MAPPING §11): a non-conforming value emits a warning and still validates. To make a format binding, use pattern or enum instead.
| JSON Schema Type | Python | Rust | Go | Java | TypeScript |
|---|---|---|---|---|---|
string |
str |
String |
string |
String |
string |
integer |
int |
i64 |
int64 |
long / Long |
number |
number |
float |
f64 |
float64 |
double / Double |
number |
boolean |
bool |
bool |
bool |
boolean / Boolean |
boolean |
null |
None |
() |
nil |
null |
null |
object (with properties) |
BaseModel subclass |
struct |
struct |
class |
z.object({}) |
additionalProperties |
dict[str, V] |
HashMap<String, V> |
map[string]V |
Map<String, V> |
Record<string, V> |
array |
list[T] |
Vec<T> |
[]T |
List<T> |
T[] |
string + format: date-time |
datetime |
DateTime<Utc> |
time.Time |
OffsetDateTime |
Date / string |
string + format: date |
date |
NaiveDate |
civil.Date |
LocalDate |
string |
string + format: time |
time |
NaiveTime |
Custom | LocalTime |
string |
string + format: email |
str + warning |
String + warning |
string + warning |
String + warning |
string + warning |
string + format: uri |
str + warning |
String + warning |
string + warning |
String + warning |
string + warning |
string + format: uuid |
UUID |
Uuid |
uuid.UUID |
UUID |
string + warning |
string enum |
Literal[...] / StrEnum |
enum |
type T string + const |
enum |
union type |
integer enum |
IntEnum |
enum |
type T int64 + iota |
enum |
union type |
oneOf |
Discriminated Union | enum (tagged) |
interface{} |
Sealed Class | discriminated union |
anyOf |
Union type | enum (untagged) |
interface{} |
Object |
union type |
T | null |
T | None |
Option<T> |
*T |
@Nullable T |
T | null |
This table is a quick reference. For complete type mapping specifications (including serialization fidelity and edge cases), see docs/spec/type-mapping.md.
=== "Python"
```python
from pydantic import BaseModel, Field
class StringConstraints(BaseModel):
# Length constraints
username: str = Field(
...,
description="Username",
min_length=3, # Minimum length
max_length=20, # Maximum length
)
# Regular expression
email: str = Field(
...,
description="Email",
pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$",
)
phone: str = Field(
...,
description="Phone number",
pattern=r"^1[3-9]\d{9}$",
)
# Format (JSON Schema standard format)
website: str = Field(
...,
description="Website",
json_schema_extra={"format": "uri"},
# Common formats: email, uri, date, time, date-time, uuid
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const StringConstraints = Type.Object({
// Length constraints
username: Type.String({
description: 'Username',
minLength: 3,
maxLength: 20,
}),
// Regular expression
email: Type.String({
description: 'Email',
pattern: '^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$',
}),
phone: Type.String({
description: 'Phone number',
pattern: '^1[3-9]\\d{9}$',
}),
// Format (JSON Schema standard format)
website: Type.String({
description: 'Website',
format: 'uri',
// Common formats: email, uri, date, time, date-time, uuid
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn string_constraints_schema() -> Value {
json!({
"type": "object",
"properties": {
// Length constraints
"username": {
"type": "string",
"description": "Username",
"minLength": 3,
"maxLength": 20
},
// Regular expression
"email": {
"type": "string",
"description": "Email",
"pattern": "^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$"
},
"phone": {
"type": "string",
"description": "Phone number",
"pattern": "^1[3-9]\\d{9}$"
},
// Format (JSON Schema standard format)
"website": {
"type": "string",
"description": "Website",
"format": "uri"
// Common formats: email, uri, date, time, date-time, uuid
}
},
"required": ["username", "email", "phone", "website"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field
class NumberConstraints(BaseModel):
# Range constraints
age: int = Field(
...,
description="Age",
ge=0, # >= 0
le=150, # <= 150
)
price: float = Field(
...,
description="Price",
gt=0, # > 0
lt=1_000_000, # < 1000000
)
# Multiple constraint
quantity: int = Field(
...,
description="Quantity (must be multiple of 10)",
multiple_of=10,
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const NumberConstraints = Type.Object({
// Range constraints (inclusive)
age: Type.Integer({
description: 'Age',
minimum: 0, // >= 0
maximum: 150, // <= 150
}),
// Range constraints (exclusive)
price: Type.Number({
description: 'Price',
exclusiveMinimum: 0, // > 0
exclusiveMaximum: 1000000, // < 1000000
}),
// Multiple constraint
quantity: Type.Integer({
description: 'Quantity (must be multiple of 10)',
multipleOf: 10,
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn number_constraints_schema() -> Value {
json!({
"type": "object",
"properties": {
// Range constraints (inclusive)
"age": {
"type": "integer",
"description": "Age",
"minimum": 0,
"maximum": 150
},
// Range constraints (exclusive)
"price": {
"type": "number",
"description": "Price",
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000000
},
// Multiple constraint
"quantity": {
"type": "integer",
"description": "Quantity (must be multiple of 10)",
"multipleOf": 10
}
},
"required": ["age", "price", "quantity"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field, field_validator
class ListConstraints(BaseModel):
# Length constraints
tags: list[str] = Field(
...,
description="Tags",
min_length=1, # At least 1
max_length=10, # At most 10
)
# Unique elements (requires custom validation in Pydantic)
unique_ids: list[str] = Field(
...,
description="Unique ID list",
)
@field_validator("unique_ids")
@classmethod
def check_unique(cls, v):
if len(v) != len(set(v)):
raise ValueError('List elements must be unique')
return v
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const ListConstraints = Type.Object({
// Length constraints
tags: Type.Array(Type.String(), {
description: 'Tags',
minItems: 1, // At least 1
maxItems: 10, // At most 10
}),
// Unique elements — JSON Schema `uniqueItems` keyword
unique_ids: Type.Array(Type.String(), {
description: 'Unique ID list',
uniqueItems: true,
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn list_constraints_schema() -> Value {
json!({
"type": "object",
"properties": {
// Length constraints
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tags",
"minItems": 1,
"maxItems": 10
},
// Unique elements — JSON Schema `uniqueItems` keyword
"unique_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Unique ID list",
"uniqueItems": true
}
},
"required": ["tags", "unique_ids"]
})
}
```
These fields help AI/LLM better understand Schema.
=== "Python"
```python
from pydantic import BaseModel, Field
class LLMFriendlyInput(BaseModel):
sql: str = Field(
...,
description="SQL statement",
json_schema_extra={
"x-llm-description": (
"SQL query statement to execute.\n"
"- Only SELECT statements allowed\n"
"- DROP, DELETE, UPDATE and other modification operations are not allowed\n"
"- Table names must use schema.table format"
),
},
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const LLMFriendlyInput = Type.Object({
sql: Type.String({
description: 'SQL statement',
'x-llm-description': [
'SQL query statement to execute.',
'- Only SELECT statements allowed',
'- DROP, DELETE, UPDATE and other modification operations are not allowed',
'- Table names must use schema.table format',
].join('\n'),
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn llm_friendly_input_schema() -> Value {
json!({
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "SQL statement",
"x-llm-description": concat!(
"SQL query statement to execute.\n",
"- Only SELECT statements allowed\n",
"- DROP, DELETE, UPDATE and other modification operations are not allowed\n",
"- Table names must use schema.table format"
)
}
},
"required": ["sql"]
})
}
```
Corresponding YAML:
sql:
type: string
description: "SQL statement"
x-llm-description: |
SQL query statement to execute.
- Only SELECT statements allowed
- DROP, DELETE, UPDATE and other modification operations are not allowed
- Table names must use schema.table format=== "Python"
```python
from pydantic import BaseModel, Field
class WithExamples(BaseModel):
email: str = Field(
...,
description="Email address",
json_schema_extra={
"x-examples": [
"user@example.com",
"admin@company.org",
"test.user@domain.co.jp",
],
},
)
phone: str = Field(
...,
description="China mainland mobile number",
pattern=r"^1[3-9]\d{9}$",
json_schema_extra={
"x-examples": ["13800138000", "15912345678"],
},
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const WithExamples = Type.Object({
email: Type.String({
description: 'Email address',
'x-examples': [
'user@example.com',
'admin@company.org',
'test.user@domain.co.jp',
],
}),
phone: Type.String({
description: 'China mainland mobile number',
pattern: '^1[3-9]\\d{9}$',
'x-examples': ['13800138000', '15912345678'],
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn with_examples_schema() -> Value {
json!({
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Email address",
"x-examples": [
"user@example.com",
"admin@company.org",
"test.user@domain.co.jp"
]
},
"phone": {
"type": "string",
"description": "China mainland mobile number",
"pattern": "^1[3-9]\\d{9}$",
"x-examples": ["13800138000", "15912345678"]
}
},
"required": ["email", "phone"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field
class WithSensitive(BaseModel):
username: str = Field(..., description="Username")
password: str = Field(
...,
description="Password",
json_schema_extra={
"x-sensitive": True, # Mark as sensitive field
},
)
api_key: str = Field(
...,
description="API key",
json_schema_extra={
"x-sensitive": True,
},
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const WithSensitive = Type.Object({
username: Type.String({ description: 'Username' }),
password: Type.String({
description: 'Password',
'x-sensitive': true, // Mark as sensitive field
}),
api_key: Type.String({
description: 'API key',
'x-sensitive': true,
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn with_sensitive_schema() -> Value {
json!({
"type": "object",
"properties": {
"username": {"type": "string", "description": "Username"},
"password": {
"type": "string",
"description": "Password",
"x-sensitive": true
},
"api_key": {
"type": "string",
"description": "API key",
"x-sensitive": true
}
},
"required": ["username", "password", "api_key"]
})
}
```
Handling of sensitive fields:
- Automatically masked in logs
- Not recorded in trace data
- AI/LLM is advised not to store
=== "Python"
```python
from pydantic import BaseModel, Field
class Address(BaseModel):
"""Address information"""
city: str = Field(..., description="City")
street: str = Field(..., description="Street")
postal_code: str = Field(..., description="Postal code")
class Company(BaseModel):
"""Company information"""
name: str = Field(..., description="Company name")
address: Address = Field(..., description="Company address") # Nested
class Employee(BaseModel):
"""Employee information"""
name: str = Field(..., description="Name")
company: Company = Field(..., description="Company") # Multi-level nesting
home_address: Address = Field(..., description="Home address") # Reuse
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// Address information — reusable
const Address = Type.Object({
city: Type.String({ description: 'City' }),
street: Type.String({ description: 'Street' }),
postal_code: Type.String({ description: 'Postal code' }),
});
// Company information
const Company = Type.Object({
name: Type.String({ description: 'Company name' }),
address: Type.Composite([Address], { description: 'Company address' }), // Nested
});
// Employee information
const Employee = Type.Object({
name: Type.String({ description: 'Name' }),
company: Type.Composite([Company], { description: 'Company' }), // Multi-level nesting
home_address: Type.Composite([Address], { description: 'Home address' }), // Reuse
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// Address information — reusable
fn address_schema() -> Value {
json!({
"type": "object",
"properties": {
"city": {"type": "string", "description": "City"},
"street": {"type": "string", "description": "Street"},
"postal_code": {"type": "string", "description": "Postal code"}
},
"required": ["city", "street", "postal_code"]
})
}
// Company information
fn company_schema() -> Value {
let mut address = address_schema();
address["description"] = json!("Company address");
json!({
"type": "object",
"properties": {
"name": {"type": "string", "description": "Company name"},
"address": address
},
"required": ["name", "address"]
})
}
// Employee information
fn employee_schema() -> Value {
let mut company = company_schema();
company["description"] = json!("Company");
let mut home = address_schema();
home["description"] = json!("Home address");
json!({
"type": "object",
"properties": {
"name": {"type": "string", "description": "Name"},
"company": company,
"home_address": home
},
"required": ["name", "company", "home_address"]
})
}
```
# Using $ref references
input_schema:
type: object
properties:
shipping_address:
$ref: "#/definitions/Address"
billing_address:
$ref: "#/definitions/Address"
definitions:
Address:
type: object
properties:
city:
type: string
street:
type: string# schemas/order.schema.yaml
input_schema:
type: object
properties:
customer:
$ref: "./common/customer.schema.yaml#/definitions/Customer"
items:
type: array
items:
$ref: "./common/product.schema.yaml#/definitions/OrderItem"For validation rules that go beyond what JSON Schema can express (cross-field checks, normalization, business rules), use the language's native validation hooks. In Python this is Pydantic's field_validator; in TypeScript and Rust the convention is to run validation inside execute() and raise/return a structured error before processing.
=== "Python"
```python
from pydantic import BaseModel, Field, field_validator
class UserInput(BaseModel):
username: str = Field(..., description="Username")
email: str = Field(..., description="Email")
password: str = Field(..., description="Password")
confirm_password: str = Field(..., description="Confirm password")
@field_validator('username')
@classmethod
def username_alphanumeric(cls, v: str) -> str:
if not v.isalnum():
raise ValueError('Username can only contain letters and numbers')
return v
@field_validator('email')
@classmethod
def email_valid(cls, v: str) -> str:
if '@' not in v:
raise ValueError('Email format is incorrect')
return v.lower() # Convert to lowercase
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// Schema declares structure and basic constraints (alphanumeric via pattern,
// email via format). Cross-field rules go in execute().
const UserInput = Type.Object({
username: Type.String({
description: 'Username',
pattern: '^[A-Za-z0-9]+$', // alphanumeric only
}),
email: Type.String({ description: 'Email', format: 'email' }),
password: Type.String({ description: 'Password' }),
confirm_password: Type.String({ description: 'Confirm password' }),
});
// Custom normalization / cross-field validation runs in execute().
function normalizeUserInput(inputs: {
username: string;
email: string;
password: string;
confirm_password: string;
}) {
if (!/^[A-Za-z0-9]+$/.test(inputs.username)) {
throw new Error('Username can only contain letters and numbers');
}
if (!inputs.email.includes('@')) {
throw new Error('Email format is incorrect');
}
return { ...inputs, email: inputs.email.toLowerCase() };
}
```
=== "Rust"
```rust
use serde_json::{json, Value};
// Schema declares structure and basic constraints. Cross-field rules
// run inside execute().
fn user_input_schema() -> Value {
json!({
"type": "object",
"properties": {
"username": {
"type": "string",
"description": "Username",
"pattern": "^[A-Za-z0-9]+$"
},
"email": {
"type": "string",
"description": "Email",
"format": "email"
},
"password": {"type": "string", "description": "Password"},
"confirm_password": {"type": "string", "description": "Confirm password"}
},
"required": ["username", "email", "password", "confirm_password"]
})
}
fn normalize_user_input(mut inputs: Value) -> Result<Value, String> {
let username = inputs["username"].as_str().unwrap_or("");
if !username.chars().all(|c| c.is_ascii_alphanumeric()) {
return Err("Username can only contain letters and numbers".into());
}
let email = inputs["email"].as_str().unwrap_or("");
if !email.contains('@') {
return Err("Email format is incorrect".into());
}
inputs["email"] = json!(email.to_lowercase());
Ok(inputs)
}
```
Model-level validation enforces rules that span multiple fields. Python uses Pydantic's model_validator; TypeScript and Rust express the same logic as a plain function invoked from execute().
=== "Python"
```python
from pydantic import BaseModel, Field, model_validator
class PasswordInput(BaseModel):
password: str = Field(..., description="Password")
confirm_password: str = Field(..., description="Confirm password")
@model_validator(mode='after')
def passwords_match(self) -> 'PasswordInput':
if self.password != self.confirm_password:
raise ValueError('Password entries do not match')
return self
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const PasswordInput = Type.Object({
password: Type.String({ description: 'Password' }),
confirm_password: Type.String({ description: 'Confirm password' }),
});
function validatePasswordInput(
inputs: { password: string; confirm_password: string },
): void {
if (inputs.password !== inputs.confirm_password) {
throw new Error('Password entries do not match');
}
}
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn password_input_schema() -> Value {
json!({
"type": "object",
"properties": {
"password": {"type": "string", "description": "Password"},
"confirm_password": {"type": "string", "description": "Confirm password"}
},
"required": ["password", "confirm_password"]
})
}
fn validate_password_input(inputs: &Value) -> Result<(), String> {
if inputs["password"] != inputs["confirm_password"] {
return Err("Password entries do not match".into());
}
Ok(())
}
```
=== "Python"
```python
from pydantic import BaseModel, Field, model_validator
class OrderItem(BaseModel):
product_id: str = Field(..., description="Product ID")
quantity: int = Field(..., description="Quantity")
price: float = Field(..., description="Unit price")
class OrderInput(BaseModel):
items: list[OrderItem] = Field(..., description="Order items")
coupon_code: str | None = Field(None, description="Coupon")
total_amount: float = Field(..., description="Total amount")
@model_validator(mode='after')
def validate_order(self) -> 'OrderInput':
# Calculate total product price
calculated_total = sum(item.price * item.quantity for item in self.items)
# Validate total amount
if abs(self.total_amount - calculated_total) > 0.01:
raise ValueError(f'Total amount is incorrect, should be {calculated_total}')
# Validate coupon
if self.coupon_code and not self._is_valid_coupon(self.coupon_code):
raise ValueError('Coupon is invalid or expired')
return self
def _is_valid_coupon(self, code: str) -> bool:
# Coupon validation logic
return True
```
=== "TypeScript"
```typescript
import { Type, type Static } from '@sinclair/typebox';
const OrderItem = Type.Object({
product_id: Type.String({ description: 'Product ID' }),
quantity: Type.Integer({ description: 'Quantity' }),
price: Type.Number({ description: 'Unit price' }),
});
const OrderInput = Type.Object({
items: Type.Array(OrderItem, { description: 'Order items' }),
coupon_code: Type.Optional(
Type.Union([Type.String(), Type.Null()], {
description: 'Coupon',
default: null,
}),
),
total_amount: Type.Number({ description: 'Total amount' }),
});
type OrderInputT = Static<typeof OrderInput>;
function isValidCoupon(_code: string): boolean {
// Coupon validation logic
return true;
}
function validateOrder(input: OrderInputT): void {
const calculatedTotal = input.items.reduce(
(sum, it) => sum + it.price * it.quantity,
0,
);
if (Math.abs(input.total_amount - calculatedTotal) > 0.01) {
throw new Error(`Total amount is incorrect, should be ${calculatedTotal}`);
}
if (input.coupon_code && !isValidCoupon(input.coupon_code)) {
throw new Error('Coupon is invalid or expired');
}
}
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn order_item_schema() -> Value {
json!({
"type": "object",
"properties": {
"product_id": {"type": "string", "description": "Product ID"},
"quantity": {"type": "integer", "description": "Quantity"},
"price": {"type": "number", "description": "Unit price"}
},
"required": ["product_id", "quantity", "price"]
})
}
fn order_input_schema() -> Value {
json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": order_item_schema(),
"description": "Order items"
},
"coupon_code": {
"type": ["string", "null"],
"description": "Coupon",
"default": null
},
"total_amount": {"type": "number", "description": "Total amount"}
},
"required": ["items", "total_amount"]
})
}
fn is_valid_coupon(_code: &str) -> bool {
// Coupon validation logic
true
}
fn validate_order(input: &Value) -> Result<(), String> {
let items = input["items"].as_array().ok_or("items must be an array")?;
let calculated: f64 = items
.iter()
.map(|it| {
it["price"].as_f64().unwrap_or(0.0)
* it["quantity"].as_f64().unwrap_or(0.0)
})
.sum();
let total = input["total_amount"].as_f64().unwrap_or(0.0);
if (total - calculated).abs() > 0.01 {
return Err(format!("Total amount is incorrect, should be {}", calculated));
}
if let Some(code) = input["coupon_code"].as_str() {
if !is_valid_coupon(code) {
return Err("Coupon is invalid or expired".into());
}
}
Ok(())
}
```
apcore supports multiple Schema loading methods:
# apcore.yaml
schema:
# Loading strategy
strategy: "yaml_first" # yaml_first | native_first | yaml_only
# yaml_first: Load from YAML first, native implementation can override
# native_first: Prefer Python class definition, YAML as fallback
# yaml_only: YAML only (pure cross-language scenarios)
# Directory the YAML schema files are resolved against
root: "./schemas"
# Maximum $ref resolution depth
max_ref_depth: 32root, strategy and max_ref_depth are the whole schema namespace —
defaults.schema.json declares it additionalProperties: false, so any other
key is a configuration error. In particular there is no schema.validation
block: whether an undeclared property is rejected, and whether "42" counts as
an integer, are properties of the contract, not of the host that loaded it
(PROTOCOL_SPEC §4.9, TYPE_MAPPING §17.3).
=== "Python"
```python
from pydantic import BaseModel, Field
# ✅ Good
class GoodSchema(BaseModel):
name: str = Field(..., description="User name, 2-50 characters")
age: int = Field(..., description="User age, 0-150 years")
# ❌ Bad
class BadSchema(BaseModel):
name: str
age: int
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// ✅ Good
const GoodSchema = Type.Object({
name: Type.String({ description: 'User name, 2-50 characters' }),
age: Type.Integer({ description: 'User age, 0-150 years' }),
});
// ❌ Bad — no descriptions
const BadSchema = Type.Object({
name: Type.String(),
age: Type.Integer(),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// ✅ Good
fn good_schema() -> Value {
json!({
"type": "object",
"properties": {
"name": {"type": "string", "description": "User name, 2-50 characters"},
"age": {"type": "integer", "description": "User age, 0-150 years"}
},
"required": ["name", "age"]
})
}
// ❌ Bad — no descriptions
fn bad_schema() -> Value {
json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel
from typing import Any, Literal
# ✅ Good: Clear types
class GoodSchema(BaseModel):
status: Literal["active", "inactive", "pending"]
count: int
price: float
# ❌ Bad: Vague types
class BadSchema(BaseModel):
status: str # Can be any string
count: Any # Unknown type
price: Any
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// ✅ Good: Clear types
const GoodSchema = Type.Object({
status: Type.Union([
Type.Literal('active'),
Type.Literal('inactive'),
Type.Literal('pending'),
]),
count: Type.Integer(),
price: Type.Number(),
});
// ❌ Bad: Vague types
const BadSchema = Type.Object({
status: Type.String(), // Can be any string
count: Type.Unknown(), // Unknown type
price: Type.Unknown(),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// ✅ Good: Clear types
fn good_schema() -> Value {
json!({
"type": "object",
"properties": {
"status": {"type": "string", "enum": ["active", "inactive", "pending"]},
"count": {"type": "integer"},
"price": {"type": "number"}
},
"required": ["status", "count", "price"]
})
}
// ❌ Bad: Vague types
fn bad_schema() -> Value {
json!({
"type": "object",
"properties": {
"status": {"type": "string"}, // Can be any string
"count": {}, // Unknown type
"price": {}
},
"required": ["status", "count", "price"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field
# ✅ Good: Has reasonable constraints
class GoodSchema(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
age: int = Field(..., ge=0, le=150)
email: str = Field(..., pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
# ❌ Bad: No constraints
class BadSchema(BaseModel):
username: str # Can be empty string or very long
age: int # Can be negative or absurd number
email: str # Can be any string
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// ✅ Good: Has reasonable constraints
const GoodSchema = Type.Object({
username: Type.String({ minLength: 3, maxLength: 20 }),
age: Type.Integer({ minimum: 0, maximum: 150 }),
email: Type.String({ pattern: '^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$' }),
});
// ❌ Bad: No constraints
const BadSchema = Type.Object({
username: Type.String(), // Can be empty string or very long
age: Type.Integer(), // Can be negative or absurd number
email: Type.String(), // Can be any string
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// ✅ Good: Has reasonable constraints
fn good_schema() -> Value {
json!({
"type": "object",
"properties": {
"username": {"type": "string", "minLength": 3, "maxLength": 20},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "pattern": "^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$"}
},
"required": ["username", "age", "email"]
})
}
// ❌ Bad: No constraints
fn bad_schema() -> Value {
json!({
"type": "object",
"properties": {
"username": {"type": "string"},
"age": {"type": "integer"},
"email": {"type": "string"}
},
"required": ["username", "age", "email"]
})
}
```
=== "Python"
```python
from pydantic import BaseModel, Field
# ✅ Good: Reusable and clear
class Address(BaseModel):
"""Address - reusable"""
city: str = Field(..., description="City")
street: str = Field(..., description="Street")
class OrderInputGood(BaseModel):
shipping_address: Address
billing_address: Address
# ❌ Bad: Duplicate definitions
class OrderInputBad(BaseModel):
shipping_city: str
shipping_street: str
billing_city: str
billing_street: str
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// ✅ Good: Reusable and clear
const Address = Type.Object({
city: Type.String({ description: 'City' }),
street: Type.String({ description: 'Street' }),
});
const OrderInputGood = Type.Object({
shipping_address: Type.Composite([Address]),
billing_address: Type.Composite([Address]),
});
// ❌ Bad: Duplicate definitions
const OrderInputBad = Type.Object({
shipping_city: Type.String(),
shipping_street: Type.String(),
billing_city: Type.String(),
billing_street: Type.String(),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// ✅ Good: Reusable and clear
fn address_schema() -> Value {
json!({
"type": "object",
"properties": {
"city": {"type": "string", "description": "City"},
"street": {"type": "string", "description": "Street"}
},
"required": ["city", "street"]
})
}
fn order_input_good_schema() -> Value {
json!({
"type": "object",
"properties": {
"shipping_address": address_schema(),
"billing_address": address_schema()
},
"required": ["shipping_address", "billing_address"]
})
}
// ❌ Bad: Duplicate definitions
fn order_input_bad_schema() -> Value {
json!({
"type": "object",
"properties": {
"shipping_city": {"type": "string"},
"shipping_street": {"type": "string"},
"billing_city": {"type": "string"},
"billing_street": {"type": "string"}
},
"required": ["shipping_city", "shipping_street", "billing_city", "billing_street"]
})
}
```
| Scenario | JSON Value | Meaning |
|---|---|---|
Field value is null |
{"name": null} |
Field exists but has no value |
| Field does not exist | {} |
Field is missing |
| Empty string | {"name": ""} |
Field exists and value is empty string |
Implementations must distinguish these three cases. The required constraint checks if the field exists, nullable controls whether null is allowed.
apcore specifies 2^53 - 1 (9007199254740991) as the cross-language integer safe boundary (determined by JavaScript Number.MAX_SAFE_INTEGER).
| Scenario | Schema Definition | Description |
|---|---|---|
| Within safe range (≤ 2^53 - 1) | type: integer |
Use directly, lossless across all languages |
| Beyond safe range | type: string + format: int64 |
64-bit integer, transmitted as string |
| Arbitrary precision integer | type: string + format: bigint |
Like blockchain nonce |
| High precision decimal | type: string + format: decimal |
Like currency, exchange rate |
=== "Python"
```python
from pydantic import BaseModel, Field
class PaymentInput(BaseModel):
# Within safe range — use int directly
user_id: int = Field(..., description="User ID", ge=0)
# Beyond safe range — use string + format
order_no: str = Field(
...,
description="Snowflake order number",
pattern=r"^\d+$",
json_schema_extra={"format": "int64"},
)
# High precision amount
amount: str = Field(
...,
description="Payment amount (accurate to cent)",
pattern=r"^-?\d+\.\d{2}$",
json_schema_extra={"format": "decimal"},
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
const PaymentInput = Type.Object({
// Within safe range — use integer directly
user_id: Type.Integer({ description: 'User ID', minimum: 0 }),
// Beyond safe range — use string + format
order_no: Type.String({
description: 'Snowflake order number',
pattern: '^\\d+$',
format: 'int64',
}),
// High precision amount
amount: Type.String({
description: 'Payment amount (accurate to cent)',
pattern: '^-?\\d+\\.\\d{2}$',
format: 'decimal',
}),
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
fn payment_input_schema() -> Value {
json!({
"type": "object",
"properties": {
// Within safe range — use integer directly
"user_id": {
"type": "integer",
"description": "User ID",
"minimum": 0
},
// Beyond safe range — use string + format
"order_no": {
"type": "string",
"description": "Snowflake order number",
"pattern": "^\\d+$",
"format": "int64"
},
// High precision amount
"amount": {
"type": "string",
"description": "Payment amount (accurate to cent)",
"pattern": "^-?\\d+\\.\\d{2}$",
"format": "decimal"
}
},
"required": ["user_id", "order_no", "amount"]
})
}
```
For complete large number type mapping specifications, see docs/spec/type-mapping.md §14.1.
- All
stringtypes must support UTF-8 encoding minLength/maxLengthshould count by Unicode characters (code points), not bytes- When containing Emoji and combining characters, implementations should use NFC normalized form
- Schema object nesting depth should not exceed 16 levels
$refrecursion depth must not exceed 32 levels- Implementations should issue warnings when detecting excessive nesting
All mainstream AI protocols (MCP, OpenAI, Anthropic, Gemini, LangChain) are based on JSON Schema. apcore's use of JSON Schema Draft 2020-12 is the correct choice. This section provides design guidelines to make Schema easier for AI/LLM to correctly understand and call.
AI/LLM accuracy significantly decreases with deeply nested structures. input_schema should keep nesting within 2-3 levels.
# ❌ Bad: Deep nesting, hard for AI to fill accurately
input_schema:
type: object
properties:
config:
type: object
properties:
database:
type: object
properties:
connection:
type: object
properties:
host:
type: string
port:
type: integer
# ✅ Good: Flat, clear for AI
input_schema:
type: object
properties:
db_host:
type: string
description: "Database server hostname or IP address"
x-examples: ["localhost", "db.example.com"]
db_port:
type: integer
description: "Database port number"
default: 5432Reference: §10.4 specifies nesting depth limit of 16 levels, but modules for AI calling should not exceed 3 levels.
Each field's description should answer "What does the AI need to know to correctly fill this field?"
| Rule | Description |
|---|---|
| Explain semantics, not type | Schema already declares type: string, description doesn't need to repeat "string type" |
| Explain value range | Like "Supported formats: png, jpg, gif" |
| Explain default behavior | Like "If not filled, send to all subscribers" |
| Explain related constraints | Like "When format is html, template_id must be provided" |
=== "Python"
```python
from pydantic import Field
# ❌ Bad: Repeats type information, no actual guidance
body_bad = Field(
...,
description="Email body, string type",
)
# ✅ Good: Explains semantics and usage
body_good = Field(
...,
description="Email body content, supports plain text or HTML. HTML format requires html=true",
)
```
=== "TypeScript"
```typescript
import { Type } from '@sinclair/typebox';
// ❌ Bad: Repeats type information, no actual guidance
const bodyBad = Type.String({
description: 'Email body, string type',
});
// ✅ Good: Explains semantics and usage
const bodyGood = Type.String({
description:
'Email body content, supports plain text or HTML. HTML format requires html=true',
});
```
=== "Rust"
```rust
use serde_json::{json, Value};
// ❌ Bad: Repeats type information, no actual guidance
fn body_bad() -> Value {
json!({"type": "string", "description": "Email body, string type"})
}
// ✅ Good: Explains semantics and usage
fn body_good() -> Value {
json!({
"type": "string",
"description": "Email body content, supports plain text or HTML. HTML format requires html=true"
})
}
```
AI protocols serialize Schema and inject it into the prompt, consuming context tokens. Reducing unnecessary token consumption can improve AI reasoning.
| Recommendation | Description |
|---|---|
| Avoid repeating type info in description | type: string already declares type, description doesn't need to mention it |
| Use x-llm-description when enum has more than 5 values | Avoid AI guessing enum meanings one by one |
| Use first sentence description in compact mode | See Schema System compact export |
| Send simplified Schema during module discovery | Load full Schema after module is selected (progressive disclosure) |
# ❌ Bad: Many enum values without explanation
status:
type: string
enum: ["draft", "pending_review", "in_review", "approved", "rejected",
"published", "archived", "suspended", "deleted"]
# ✅ Good: Use x-llm-description to explain usage scenarios
status:
type: string
enum: ["draft", "pending_review", "in_review", "approved", "rejected",
"published", "archived", "suspended", "deleted"]
description: "Article status"
x-llm-description: |
Article lifecycle status. Use draft when creating, pending_review for submission,
approved after review, published for going live.
Other statuses (archived/suspended/deleted) are for admin operations, generally not used during creation.In the properties block, list required fields (required) first, then optional fields. This helps AI grasp core parameters faster when reading Schema.
# ✅ Recommended: Required fields first
input_schema:
type: object
properties:
# --- Required fields ---
to:
type: string
description: "Recipient email address"
subject:
type: string
description: "Email subject"
body:
type: string
description: "Email body"
# --- Optional fields ---
cc:
type: array
items: { type: string }
description: "CC list"
default: []
html:
type: boolean
description: "Is HTML format"
default: false
required: [to, subject, body]Before publishing modules for AI calling, it's recommended to check against the following checklist:
- input_schema nesting does not exceed 3 levels
- Every field has description, and does not repeat type information
- Required fields are listed before optional fields in properties
- Provide x-llm-description when enum has more than 5 values
- Complex modules (oneOf/anyOf, 5+ required fields) provide examples
- Sensitive fields marked with x-sensitive
- Numeric fields declare
minimum/maximumanddefaultwhere applicable (Pydantic:Field(ge=, le=)) - String fields with fixed options use
enumorLiteral[...]instead of free-form text
apcore uses JSON Schema Draft 2020-12, but some AI protocols (like MCP) are still based on Draft 7. Modules aimed at broad AI protocol compatibility should prioritize using the common subset of both versions.
The following keywords can be safely used in all mainstream AI protocols:
| Keyword | Description |
|---|---|
type |
Type declaration |
properties |
Object property definition |
required |
Required field list |
enum |
Enum values |
const |
Constant value |
description |
Field description |
default |
Default value |
minimum / maximum |
Numeric range |
minLength / maxLength |
String length |
pattern |
Regular expression constraint |
items |
Array element definition |
$ref |
Reference (local reference) |
oneOf / anyOf / allOf |
Combined types |
additionalProperties |
Allow additional properties |
format |
Format annotation (like date-time, email) |
The following keywords are only available in Draft 2020-12, some AI protocols may not support:
| Keyword | Description | Risk |
|---|---|---|
if / then / else |
Conditional Schema | MCP Draft 7 doesn't support; some AI protocols ignore |
$dynamicRef / $dynamicAnchor |
Dynamic reference | Most AI protocols don't support |
prefixItems |
Tuple definition (replaces old items array form) |
Draft 7 uses items array form |
$anchor |
Named anchor | Draft 7 uses $id |
dependentRequired / dependentSchemas |
Dependency declaration | Draft 7 uses dependencies |
- Modules aimed at broad AI protocol compatibility SHOULD limit to the safe subset in §12.1
- Modules used only within apcore and not exported to external AI protocols can freely use all Draft 2020-12 features
- When using
oneOf/anyOf, SHOULD provideexamplesto help AI understand branch meanings - Cross-file
$refSHOULD be inlined when exported, as most AI protocols don't support external reference resolution
- Creating Modules Guide - Complete module creation tutorial
- Module Interface - Module Protocol contract
- ACL Configuration Guide - Access control configuration