> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blockradar.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Dynamic Forms

> Render and validate forms from schemas returned by the Blockradar API.

Some Blockradar endpoints return a schema that describes the information required
to continue an operation. Use the schema to render fields, validate user input,
and submit the collected data to the API.

Your implementation must preserve the schema's field names, validation rules,
and submitted value types.

<Note>
  Blockradar form schemas follow [JSON Schema
  draft-07](https://json-schema.org/draft-07). This guide covers the keywords used
  by Blockradar and the additional `x-*` directives that control form behavior. You
  do not need to support every draft-07 keyword.
</Note>

## How it works

<Steps>
  <Step title="Request the requirements">
    Call the relevant discovery or requirements endpoint. The response identifies
    whether more information is required and includes a schema when it is.
  </Step>

  <Step title="Render the form">
    Walk through the schema's `properties` and choose a control for each field.
    Use the containing object's `required` array to mark required fields.
  </Step>

  <Step title="Resolve dependent fields">
    If a field uses a Blockradar resolution directive, wait for its dependencies
    and call the operation referenced by the field. The requirements response
    provides the operation's HTTP method and API-relative URL.
  </Step>

  <Step title="Validate the values">
    Validate the completed value against the schema and translate validation
    failures into clear, customer-facing errors.
  </Step>

  <Step title="Submit the result">
    Send an object with the same property names and value types under the request
    property documented by the endpoint, such as `paymentMethodData` or
    `additionalData`.
  </Step>
</Steps>

## Render your first form

The following schema describes two required bank-account fields:

```json theme={null}
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Bank account details",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "bankCode": {
      "type": "string",
      "title": "Bank"
    },
    "accountNumber": {
      "type": "string",
      "title": "Account number",
      "pattern": "^[0-9]{10}$"
    }
  },
  "required": ["bankCode", "accountNumber"]
}
```

Render a text input for each property. When both values are valid, submit:

```json theme={null}
{
  "bankCode": "058",
  "accountNumber": "0123456789"
}
```

<Warning>
  Requiredness belongs to the containing object. A field is required when its name
  appears in that object's `required` array. A field-level `required: true` is not
  part of JSON Schema draft-07.
</Warning>

## Map schemas to form controls

Use the following mapping as a starting point. Choose components that match your
application's design system.

| Schema                           | Suggested control                         | Submitted value        |
| -------------------------------- | ----------------------------------------- | ---------------------- |
| `type: "string"`                 | Text input                                | String                 |
| `type: "string", format: "date"` | Date picker                               | Date string            |
| `type: "number"`                 | Number input                              | Number                 |
| `type: "boolean"`                | Checkbox or true/false choice             | Boolean                |
| `enum`                           | Select, radio group, or segmented control | Selected value         |
| Primitive `oneOf` choices        | Labeled select or radio group             | Selected `const` value |
| `type: "object"`                 | Nested form section                       | Object                 |
| Array of strings                 | Repeatable input or multi-select          | Array of strings       |
| Array of objects                 | Repeatable form section                   | Array of objects       |
| `readOnly: true`                 | Disabled or non-editable field            | Resolved value         |

Use `title` as the visible label and `description` as supporting or help text.
Preserve the property key as the submitted field name.

### Enforce validation rules

Apply the validation keywords supported by each field type:

| Keyword                  | Applies to | Meaning                                    |
| ------------------------ | ---------- | ------------------------------------------ |
| `required`               | Object     | Names the properties that must be present  |
| `pattern`                | String     | Regular expression the value must match    |
| `format`                 | String     | Semantic format such as `date`             |
| `minLength`, `maxLength` | String     | Minimum and maximum string length          |
| `minimum`, `maximum`     | Number     | Inclusive numeric limits                   |
| `minItems`, `maxItems`   | Array      | Minimum and maximum number of items        |
| `additionalProperties`   | Object     | Whether undeclared properties are accepted |

Use client-side validation to show errors before submission. The Blockradar API
also validates the submitted data and returns an error when it does not match the
schema.

## Render choices

Schemas express choices with `enum`, `const`, and `oneOf`. Each keyword has a
different purpose.

### Simple values with `enum`

Use `enum` when the stored values are suitable as labels:

```json theme={null}
{
  "type": "string",
  "title": "Account type",
  "enum": ["checking", "savings"]
}
```

### Labeled values with `oneOf` and `const`

Use `oneOf` when the displayed label differs from the submitted value:

```json theme={null}
{
  "type": "string",
  "title": "Account type",
  "oneOf": [
    {
      "const": "checking",
      "title": "Checking account"
    },
    {
      "const": "savings",
      "title": "Savings account"
    }
  ]
}
```

The renderer displays the branch `title` and submits its `const` value.

### Different forms with `oneOf`

An object-level `oneOf` represents alternative form shapes. Blockradar uses a
required `type` property with a unique `const` value to identify each branch.
Use the branch `title` as the visible option label.

```json theme={null}
{
  "type": "object",
  "title": "Recipient",
  "oneOf": [
    {
      "title": "Individual",
      "properties": {
        "type": { "type": "string", "const": "individual" },
        "firstName": { "type": "string", "title": "First name" }
      },
      "required": ["type", "firstName"]
    },
    {
      "title": "Business",
      "properties": {
        "type": { "type": "string", "const": "business" },
        "businessName": { "type": "string", "title": "Business name" }
      },
      "required": ["type", "businessName"]
    }
  ]
}
```

Render a selector from the branch titles. When a user selects a branch, set its
`type.const` value, remove values that belong only to the previous branch, and
render the selected branch's remaining properties.

<Warning>
  Do not use `discriminator`. It is not part of JSON Schema draft-07. Every
  selectable object branch must define a unique `type.const` value and include
  `type` in its `required` array.
</Warning>

<Tip>
  Think of `enum` as a list of values, `const` as one fixed value, and `oneOf` as
  one matching schema. Do not replace every `oneOf` with `enum`: object branches
  can contain different fields and validation rules.
</Tip>

## Render conditional fields

Calculate the active schema before rendering its fields:

* `allOf` applies every listed schema.
* `oneOf` applies exactly one matching schema.
* `if` evaluates a condition, then applies either `then` or `else`.

This example asks for a registration number only when `customerType` is
`business`:

```json theme={null}
{
  "type": "object",
  "properties": {
    "customerType": {
      "type": "string",
      "title": "Customer type",
      "enum": ["individual", "business"]
    }
  },
  "required": ["customerType"],
  "if": {
    "properties": {
      "customerType": { "const": "business" }
    }
  },
  "then": {
    "properties": {
      "registrationNumber": {
        "type": "string",
        "title": "Registration number"
      }
    },
    "required": ["registrationNumber"]
  }
}
```

Re-evaluate conditions whenever a dependency changes. Remove values belonging to
an inactive branch before submission unless an endpoint explicitly says to
retain them.

## Blockradar extensions

Properties beginning with `x-` extend JSON Schema draft-07 with Blockradar form
behavior. Standard JSON Schema validators can ignore these properties, but your
renderer must handle any extension used by a required field.

| Extension                   | Purpose                                                                 |
| --------------------------- | ----------------------------------------------------------------------- |
| `x-field-kind`              | Selects a specialized field such as a resolved value or document upload |
| `x-data-source`             | Identifies the source for remotely populated choices                    |
| `x-data-source-depends-on`  | Lists values required before loading a data source                      |
| `x-resolution`              | References an API operation that resolves a field from other values     |
| `x-accept`                  | Lists accepted upload MIME types                                        |
| `x-max-file-size-bytes`     | Sets the maximum file size in bytes                                     |
| `x-document-type`           | Assigns a fixed document type                                           |
| `x-document-types`          | Lists the document types a user can upload                              |
| `x-document-upload-mode`    | Identifies how the document must be uploaded                            |
| `x-upload-response-mapping` | Maps an upload response to the schema's document value                  |

<Warning>
  Do not hide a required field when its `x-field-kind` is unsupported. Stop the
  flow and surface an integration error instead of submitting incomplete data.
</Warning>

### Validation errors

Generate clear messages from the standard keyword that failed. For example, a
`pattern` failure can become "Enter a valid account number," while a `minLength`
failure can include the required length. For a missing property, map the
object's `required` error back to the corresponding field and its `title`.

### Resolved fields

A resolved field receives its value from a Blockradar API operation after its
dependencies have valid values. Its `x-resolution.operation` references an entry
in the response-level `operations` map.

The requirements response contains the schema and its available operations:

```json theme={null}
{
  "paymentMethodId": "pm_123",
  "schema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "institutionIdentifier": {
        "type": "string",
        "title": "Institution",
        "oneOf": [
          {
            "const": "058",
            "title": "Example Bank"
          }
        ]
      },
      "accountIdentifier": {
        "type": "string",
        "title": "Account number"
      },
      "accountName": {
        "type": "string",
        "title": "Account name",
        "readOnly": true,
        "x-field-kind": "resolved",
        "x-resolution": {
          "operation": "resolvePaymentMethod",
          "dependsOn": [
            "institutionIdentifier",
            "accountIdentifier"
          ],
          "responsePath": "accountName"
        }
      }
    },
    "required": [
      "institutionIdentifier",
      "accountIdentifier"
    ]
  },
  "operations": {
    "resolvePaymentMethod": {
      "method": "POST",
      "href": "/v2/wallets/862f0000-0000-0000-0000-000000000000/withdraw/fiat/payment-method/resolve"
    }
  },
  "metadata": null
}
```

The resolution directive contains:

| Property       | Type      | Description                                                 |
| -------------- | --------- | ----------------------------------------------------------- |
| `operation`    | string    | Key of the operation in the response-level `operations` map |
| `dependsOn`    | string\[] | Field paths that must be valid before calling the operation |
| `responsePath` | string    | Path to the resolved value in the operation response data   |

Each operation contains:

| Property | Type   | Description                                                                 |
| -------- | ------ | --------------------------------------------------------------------------- |
| `method` | string | HTTP method to use. Dynamic form resolution operations currently use `POST` |
| `href`   | string | Operation path with route parameters already resolved                       |

For a Withdraw Fiat resolution, send the current form data with the payment
context:

```json theme={null}
{
  "assetId": "asset-uuid",
  "currency": "NGN",
  "amount": "1000",
  "paymentMethod": "pm_123",
  "paymentMethodData": {
    "institutionIdentifier": "058",
    "accountIdentifier": "0123456789"
  }
}
```

Your application should:

1. Wait until every path in `dependsOn` has a value.
2. Validate each dependency using its schema rules.
3. Find the named `operation` in the response-level `operations` map.
4. Call its `href` using its `method` and the request body documented by the endpoint.
5. Read the resolved value from the response data using `responsePath`.
6. Store and display the result in the read-only field.
7. Clear the resolved value and resolve it again whenever a dependency changes.
8. Prevent submission while a required resolution is pending or has failed.

If multiple fields reference the same operation, call it once for the current
dependency values and apply each field's `responsePath` to the returned data.

When dependencies change, cancel the previous request when possible or ignore
its response. A stale response must not overwrite a newer resolved value.

## File and document fields

File and document extensions describe the required upload. Use only the
Blockradar upload operation associated with the form flow; never treat a schema
value as an arbitrary upload destination.

```json theme={null}
{
  "identityDocument": {
    "type": "object",
    "title": "Identity document",
    "x-field-kind": "file",
    "x-accept": ["application/pdf", "image/jpeg", "image/png"],
    "x-max-file-size-bytes": 5000000,
    "x-document-types": ["passport", "national_id"]
  }
}
```

Enforce the declared file restrictions, show upload progress, map the successful
upload response, and submit the mapped document value. Do not submit the
browser's local `File` object.

## Implement the renderer

Separate schema validation from UI rendering. Use a draft-07-compatible validator
for standard constraints and handle Blockradar extensions in your rendering
layer.

```ts theme={null}
async function submitDynamicForm(schema, values) {
  const normalizedValues = normalizeValues(schema, values);
  const validation = validateDraft07(schema, normalizedValues);

  if (!validation.valid) {
    showFieldErrors(mapSchemaErrors(validation.errors, schema));
    return;
  }

  await submitToBlockradar(normalizedValues);
}
```

Resolve fields through the operation map rather than a hardcoded endpoint:

```ts theme={null}
async function resolveField(fieldSchema, operations, payload) {
  const resolution = fieldSchema["x-resolution"];
  const operation = operations[resolution.operation];

  assertSupportedOperation(operation);

  const response = await blockradar.request({
    method: operation.method,
    url: operation.href,
    data: payload,
  });

  return getValueAtPath(response.data, resolution.responsePath);
}
```

A recursive renderer can follow this outline:

```ts theme={null}
function renderSchema(schema, values, path = []) {
  const resolvedSchema = applyConditionsAndComposition(schema, values);

  if (resolvedSchema.type === "object") {
    return Object.entries(resolvedSchema.properties ?? {}).map(
      ([key, fieldSchema]) =>
        renderField(fieldSchema, values[key], [...path, key], {
          required: (resolvedSchema.required ?? []).includes(key),
        }),
    );
  }

  return renderPrimitive(resolvedSchema, values, path);
}
```

The function names are illustrative. Use an established JSON Schema library for
validation and schema evaluation where possible.

### Generate a starting point with an LLM

You can share this guide—or the URL of this page—with an LLM together with a
schema returned by Blockradar and your frontend conventions. Use the following
prompt as a starting point:

```text theme={null}
Build a reusable dynamic form renderer for this application.

Requirements:
- Read and follow the Blockradar Dynamic Forms guide provided with this prompt.
- Treat the supplied schema as JSON Schema draft-07 with the Blockradar x-*
  directives documented in the guide.
- Use our existing form, input, select, date, and upload components.
- Validate standard constraints with a JSON Schema draft-07 validator.
- Implement recursive objects, arrays, enum choices, labeled oneOf choices,
  allOf, and if/then/else.
- Select object-level oneOf branches using their required, unique type.const values.
- Do not implement or depend on discriminator.
- Keep Blockradar x-* directives in a separate adapter layer.
- Implement x-resolution fields according to the behavior described in the guide.
- Re-evaluate conditional fields and resolved values when their dependencies change.
- Never hide an unsupported required field.
- Produce a normalized submission object that preserves the schema's property
  names and value types.

Before writing code, list any schema feature that the current component library
cannot represent safely.
```

<Tip>
  Include your framework, form library, component conventions, and one real schema
  returned by Blockradar. Do not ask the LLM to invent undocumented `x-*`
  directives.
</Tip>

## Implementation checklist

* Preserve property names and submitted value types.
* Determine required fields from the containing object's `required` array.
* Re-evaluate conditional schemas when values change.
* Validate active fields before submission.
* Remove values from inactive conditional branches.
* Select object-level `oneOf` branches using their required, unique `type.const` values.
* Do not depend on `discriminator`.
* Treat `readOnly` fields as non-editable.
* Resolve operations from the response-level `operations` map.
* Clear resolved values when their dependencies change.
* Ignore stale resolution responses.
* Block submission while required uploads or resolutions are incomplete.
* Reject unsupported required field kinds instead of hiding them.
* Validate again on the server for applications with their own backend.

## Related guides

<CardGroup cols={2}>
  <Card title="Deposit Fiat" icon="building-columns" href="/en/use-cases/virtual-accounts">
    Create and manage virtual accounts for fiat deposits.
  </Card>

  <Card title="Withdraw Fiat" icon="money-bill-transfer" href="/en/use-cases/withdraw-fiat">
    Convert stablecoins and send fiat to supported destinations.
  </Card>
</CardGroup>
