# Method Schema Specification

**Status**: Draft
**Date**: 2025-01-04
**Last Updated**: 2026-01-04

## Overview

This document defines the contract between substrate plugins and clients (synapse, TypeScript SDK, etc.). It specifies the supported JSON Schema patterns for method parameters and return types, enabling:

1. **Plugin validation** - Verify schemas conform to the spec
2. **Client implementation** - Clients can reliably parse schemas and build requests
3. **Code generation** - Transpilers produce correct typed clients

## Method Schema Structure

Each method exposes a schema with this structure:

```json
{
  "name": "method_name",
  "description": "Human-readable description",
  "hash": "content-addressable hash for versioning",
  "params": { /* JSON Schema */ },
  "returns": { /* JSON Schema */ },
  "streaming": true | false
}
```

## Supported Type Patterns

### 1. Primitives

Basic scalar types.

```json
{"type": "string"}
{"type": "integer"}
{"type": "number"}
{"type": "boolean"}
```

With optional format hints:

```json
{"type": "string", "format": "uuid"}
{"type": "integer", "format": "int64"}
```

**Client behavior**: Map to language primitives. Use format hints for validation/display.

### 2. Optional Types

Two equivalent representations:

```json
{"type": ["string", "null"]}
```

```json
{"type": "string", "default": null}
```

**Client behavior**:
- CLI: Omit flag if not provided
- Request: Either omit field or send `null` (substrate accepts both)

### 3. Arrays

```json
{
  "type": "array",
  "items": { /* element type */ }
}
```

**Client behavior**: Accept multiple values or JSON array syntax.

### 4. Objects (Structs)

```json
{
  "type": "object",
  "properties": {
    "field1": { /* type */ },
    "field2": { /* type */ }
  },
  "required": ["field1"]
}
```

**Client behavior**: Build nested object. Required fields must be present.

### 5. References ($ref)

References to types defined in `$defs`:

```json
{
  "$ref": "#/$defs/TypeName"
}
```

With the definition in the schema root:

```json
{
  "$defs": {
    "TypeName": { /* type definition */ }
  }
}
```

**Client behavior**: Resolve reference, apply the referenced type's rules.

### 6. Tagged Unions (Discriminated Enums)

The most complex pattern. Used for sum types with a discriminator field.

```json
{
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "type": {"const": "variant_a", "type": "string"},
        "field1": {"type": "string"}
      },
      "required": ["type", "field1"]
    },
    {
      "type": "object",
      "properties": {
        "type": {"const": "variant_b", "type": "string"},
        "field2": {"type": "integer"}
      },
      "required": ["type", "field2"]
    }
  ]
}
```

**Discriminator detection**:
1. Find fields with `{"const": "..."}` pattern
2. The field name is the discriminator (typically `"type"`)
3. The const value identifies the variant

**Client behavior**:
- Help: Show variants and their fields
- Input: Accept variant name + fields, or full JSON
- Request: Build `{"type": "variant_a", "field1": "value"}`

### 7. String Enums

Simple enumeration of string values:

```json
{"enum": ["pending", "completed", "failed"]}
```

Or via oneOf with const:

```json
{
  "oneOf": [
    {"const": "pending", "type": "string"},
    {"const": "completed", "type": "string"},
    {"const": "failed", "type": "string"}
  ]
}
```

**Client behavior**: Validate input is one of the allowed values.

### 8. Any/Dynamic

Intentionally dynamic content (e.g., serde_json::Value):

```json
{}
```

Or:

```json
true
```

**Client behavior**: Accept any valid JSON.

### 9. Maps/Dictionaries

Used for dynamic key-value pairs (e.g., `HashMap<String, T>`):

```json
{
  "type": "object",
  "additionalProperties": { /* value type */ }
}
```

Example from Arbor's `ResourceRefs.owners`:
```json
{
  "type": "object",
  "additionalProperties": {"type": "integer", "format": "int64"}
}
```

**Client behavior**: Accept JSON object with string keys and typed values.

**Note**: Maps with non-string keys are NOT supported in JSON Schema.

### 10. Numeric Types with Format

Integer and number types with format hints for precision:

```json
{"type": "integer", "format": "int64"}
{"type": "integer", "format": "uint32"}
{"type": "number"}
```

Common formats:
- `int32`, `int64`: Signed integers
- `uint32`, `uint64`: Unsigned integers (minimum: 0)
- No format on `number`: f64 (IEEE 754 double)

**Client behavior**: Use language-appropriate numeric types. Validate ranges if format specifies unsigned.

### 11. anyOf (Optional References)

Used for optional references to complex types:

```json
{
  "anyOf": [
    {"$ref": "#/$defs/Position"},
    {"type": "null"}
  ]
}
```

**Client behavior**: Resolve the non-null option, treat as optional.

**Note**: Synapse's IR Builder handles this pattern specially to produce `RefOptional(RefNamed(...))`.

### 12. Conditional Serialization

Fields with `#[serde(skip_serializing_if = "...")]` may be omitted from responses.

Common patterns:
- `Option::is_none` - Field omitted when None
- `Vec::is_empty` - Field omitted when empty array

**Schema representation**:
```json
{
  "field": {"type": "array", "items": {...}, "default": []}
}
```

**Client behavior**: Treat missing fields as their default values (null/empty).

## Client Request Building Algorithm

For each parameter:

1. **Primitive**: Convert CLI value to appropriate JSON type
2. **Optional**: If not provided, omit or send null
3. **Array**: Collect multiple values into JSON array
4. **Object**: Recursively build nested object
5. **$ref**: Resolve and apply referenced type
6. **Tagged Union**:
   a. If input is valid JSON object with discriminator, use as-is
   b. If input is simple value, infer variant:
      - For `ConeIdentifier`: UUID format → `by_id`, else → `by_name`
      - Build: `{"type": "variant", ...fields}`
7. **String Enum**: Validate against allowed values
8. **Any**: Pass through as-is

## Help Display Algorithm

For each parameter:

1. **Primitive**: Show `<type>` (e.g., `<string>`, `<integer>`)
2. **Optional**: Append `?` or show `(optional)`
3. **$ref**: Resolve and show referenced type
4. **Tagged Union**: Show `<variant1|variant2|...>` or expand variants
5. **String Enum**: Show `<val1|val2|val3>`
6. **Any**: Show `<any>` or `<json>`

## Validation Rules for Plugins

Plugins MUST:

1. Use only the patterns defined above
2. Include `description` for all methods and parameters
3. Use `required` array to indicate mandatory fields
4. Define all referenced types in `$defs`
5. Use `"type"` as the discriminator field name for tagged unions

Plugins SHOULD:

1. Prefer simple types over complex nested structures for CLI usability
2. Provide `format` hints for special string types (uuid, uri, date-time)
3. Include `default` values where sensible

## Implementation Status

| Pattern | Substrate | Synapse IR | hub-codegen TS |
|---------|-----------|------------|----------------|
| Primitives | ✅ | ✅ | ✅ |
| Optional | ✅ | ✅ | ✅ |
| Arrays | ✅ | ✅ | ✅ |
| Objects (Structs) | ✅ | ✅ | ✅ |
| $ref | ✅ | ✅ | ✅ |
| Tagged Unions | ✅ | ✅ | ✅ |
| String Enums | ✅ | ✅ | ✅ |
| Any/Dynamic | ✅ | ✅ | ✅ |
| Maps (additionalProperties) | ✅ | ❌ | ❌ |
| anyOf (optional refs) | ✅ | ✅ | ✅ |
| Numeric formats (int64, uint32) | ✅ | ✅ | ✅ |

**Legend**:
- **Substrate**: Schema generation via schemars
- **Synapse IR**: Haskell IR Builder extracts types correctly
- **hub-codegen TS**: TypeScript code generation

**Notes**:
- Synapse IR now builds from schema tree walk, producing a global IR with deduplicated types
- Maps with `additionalProperties` are not yet supported in Synapse IR or hub-codegen
- Synapse CLI (help/request building) is separate from IR; see Synapse.Algebra.Render

## Next Steps

1. **Maps support**: Add `additionalProperties` handling to Synapse IR and hub-codegen
2. **Validation**: Add schema validation to hub-macro or runtime
3. **Testing**: Add conformance tests for all patterns
4. **Documentation**: Generate from spec, not ad-hoc

## Serde Attributes Reference

Substrate plugins use these serde attributes that affect schema generation:

| Attribute | Effect | Example |
|-----------|--------|---------|
| `#[serde(tag = "type")]` | Internal tagging for enums | Creates `{"type": "variant_name", ...}` |
| `#[serde(rename_all = "snake_case")]` | Case conversion | `VariantName` becomes `"variant_name"` |
| `#[serde(rename = "...")]` | Explicit rename | Override variant/field name |
| `#[serde(skip_serializing_if = "...")]` | Conditional serialization | Field may be absent |
| `#[serde(default)]` | Default on deserialize | Enables `default` in schema |
| `#[serde(skip)]` | Exclude from schema | Field not exposed |

### Tagging Patterns

**Internal tagging** (used by all substrate plugins):
```rust
#[serde(tag = "type")]
pub enum MyEvent {
    #[serde(rename = "my_variant")]
    MyVariant { field: String },
}
```

Produces:
```json
{"type": "my_variant", "field": "value"}
```

**Combined rename_all + tag** (most common pattern):
```rust
#[serde(tag = "type", rename_all = "snake_case")]
pub enum HealthEvent {
    Status { status: String, uptime_seconds: u64 },
}
```

## Type Aliases and Custom Schemas

### Type Aliases

Type aliases like `pub type TreeId = ArborId` are transparent in JSON Schema - they serialize as their underlying type.

**Rust**:
```rust
pub type TreeId = ArborId;
pub type NodeId = ArborId;
```

**Schema**: Both appear as the underlying ArborId schema.

### Custom JsonSchema Implementations

Types with custom `impl JsonSchema` can control their schema representation.

**Example**: ArborId wraps UUID but provides a simple string schema:
```rust
impl JsonSchema for ArborId {
    fn schema_name() -> std::borrow::Cow<'static, str> {
        "UUID".into()
    }
    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
        gen.subschema_for::<String>()  // Emit as string, not object
    }
}
```

**Client behavior**: Treat as the schema indicates (string in this case), not the Rust type.

## Patterns to Avoid

These patterns are NOT recommended for substrate plugins:

1. **External tagging** (`#[serde(tag = "type", content = "data")]`) - Creates nested structure, harder for clients
2. **Untagged enums** (`#[serde(untagged)]`) - No discriminator, harder to parse
3. **Adjacently tagged** (`#[serde(tag = "t", content = "c")]`) - Different wire format
4. **Maps with non-string keys** - Not supported in JSON
5. **Deeply nested generics** - Schema becomes complex

## Examples

### Example 1: Simple Method

```json
{
  "name": "echo",
  "params": {
    "properties": {
      "message": {"type": "string", "description": "Text to echo"},
      "count": {"type": "integer", "default": 1, "description": "Repeat count"}
    },
    "required": ["message"]
  }
}
```

CLI: `synapse substrate echo echo --message "hello" --count 3`
Request: `{"message": "hello", "count": 3}`

### Example 2: Tagged Union Parameter

```json
{
  "name": "get",
  "params": {
    "$defs": {
      "ConeIdentifier": {
        "oneOf": [
          {
            "properties": {
              "type": {"const": "by_name"},
              "name": {"type": "string"}
            },
            "required": ["type", "name"]
          },
          {
            "properties": {
              "type": {"const": "by_id"},
              "id": {"type": "string", "format": "uuid"}
            },
            "required": ["type", "id"]
          }
        ]
      }
    },
    "properties": {
      "identifier": {"$ref": "#/$defs/ConeIdentifier"}
    },
    "required": ["identifier"]
  }
}
```

CLI: `synapse substrate cone get --identifier haiku35`
Request: `{"identifier": {"type": "by_name", "name": "haiku35"}}`

CLI: `synapse substrate cone get --identifier c816981f-ce77-418b-aec9-7b844d03a0d1`
Request: `{"identifier": {"type": "by_id", "id": "c816981f-ce77-418b-aec9-7b844d03a0d1"}}`
