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

# Render dynamic forms in your application

> Render Uphold dynamic forms in your app using the JSON Schema, UI Schema, and submission handlers, with examples for React, JSON Forms, and validation.

This page provides implementation guidance for rendering dynamic forms in your application.

## Getting started

To render a dynamic form, you need:

1. A **schema** — the JSON Schema defining data types and validation
2. A **uiSchema** — the UI Schema defining layout and controls
3. A **data** — (optional) existing data to prefill the form
4. A **renderer** — either a JSON Forms library or your own implementation

## Using JSON Forms libraries

The fastest way to get started is using an official JSON Forms renderer:

* [React](https://jsonforms.io/docs/integrations/react/)
* [Angular](https://jsonforms.io/docs/integrations/angular)
* [Vue](https://jsonforms.io/docs/integrations/vue)

These libraries handle schema interpretation, validation, and rendering out of the box.

### Schema-level hints

The JSON Schema includes a `format` keyword that indicates how a field should be rendered. Use it to select the appropriate input component:

| Format           | Implementation             |
| ---------------- | -------------------------- |
| `format: "date"` | Render a date picker input |

### Custom renderers for Enterprise API Suite

The UI Schema extends JSON Forms with custom `options` that require additional rendering logic:

| Option                  | Implementation                                                                                                                                                 |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.source`           | Populate options from any data source (API, database, etc.) based on the source identifier. The Enterprise API Suite provides endpoints for available sources. |
| `data.exclude`          | Filter the data source based on the specified restriction                                                                                                      |
| `format: "postal-code"` | Render a postal code input with country-aware formatting                                                                                                       |
| `rules`                 | Apply client-side validation rules (e.g., age thresholds)                                                                                                      |
| `dependsOn`             | Re-fetch or update the control when a dependency value changes                                                                                                 |

## Building a custom renderer

If you need full control over the user experience, you can build your own form renderer. Your implementation must:

1. **Parse the schema** — Extract property definitions, types, and validation rules
2. **Parse the uiSchema** — Build the layout tree from elements
3. **Render controls** — Map schema types to appropriate input components
4. **Apply rules** — Evaluate conditions and show/hide/enable/disable elements
5. **Validate input** — Enforce schema constraints before submission
6. **Handle progressive disclosure** — Re-render when the API returns updated schemas

## Handling progressive disclosure

When implementing progressive disclosure:

1. Render the form from `schema`, `uiSchema`, and any existing `data`
2. Collect user input for the current step (Category)
3. Submit the answers to the API
4. Compare the new response with the previous one:
   * If there's a new root property in the schema, or a new Category in the uiSchema, re-render the form to show the new questions
   * If the schema and uiSchema are unchanged, the form is complete

## Examples: React custom renderers

The examples below show how to implement custom renderers using [JSON Forms for React](https://jsonforms.io/docs/integrations/react/) and Material UI. Each renderer follows the same pattern: a **tester** that matches specific UI Schema options, and a **control component** that renders the appropriate input.

### Country renderer (`data.source: "countries"`)

Fetches the country list and applies `data.exclude` filters.
In this example, the data is fetched from [List countries](/rest-apis/core-api/countries/list-countries) endpoint.

```tsx expandable theme={null}
import { withJsonFormsControlProps } from '@jsonforms/react';
import { rankWith } from '@jsonforms/core';
import { Autocomplete, TextField } from '@mui/material';
import { useEffect, useState } from 'react';

// Custom renderer that fetches data for the data.source option
const DataSourceControl = (props) => {
  const { data, path, handleChange, uischema, label } = props;
  const [options, setOptions] = useState([]);
  const { data: dataOptions } = uischema.options || {};

  useEffect(() => {
    if (dataOptions?.source === 'countries') {
      // Fetch countries and apply exclude filters
      fetch('https://api.enterprise.uphold.com/core/countries', {
        headers: {
          'Authorization': `Bearer ${accessToken}` // Your OAuth2 access token
        }
      })
        .then((res) => res.json())
        .then((countries) => {
          // Apply exclude filter if specified
          const filtered = dataOptions.exclude?.restrictions
            ? countries.filter((country) =>
                !dataOptions.exclude.restrictions.some((r) =>
                  country.restrictions?.some((cr) => cr.scope === r.scope)
                )
              )
            : countries;
          setOptions(filtered.map((c) => ({ value: c.code, label: c.name })));
        });
    }
  }, [dataOptions]);

  return (
    <Autocomplete
      options={options}
      value={options.find((o) => o.value === data) || null}
      onChange={(_, selected) => handleChange(path, selected?.value)}
      getOptionLabel={(option) => option.label}
      renderInput={(params) => <TextField {...params} label={label} />}
    />
  );
};

// Tester: use this renderer when data.source is present
const dataSourceTester = rankWith(10, (uischema) =>
  uischema.options?.data?.source ? true : false
);

// Export for use with JsonForms
export const dataSourceRenderer = {
  tester: dataSourceTester,
  renderer: withJsonFormsControlProps(DataSourceControl),
};
```

This example demonstrates:

* Detecting the `data.source` option with a custom tester
* Fetching options from the API and applying `data.exclude` filters
* Using Material UI's Autocomplete for consistent styling
* Passing existing `data` to prefill the form

### Subdivision renderer (`data.source: "subdivisions"` + `dependsOn`)

Fetches subdivisions based on the selected country. Uses `dependsOn` to read the current country value and re-fetch when it changes.
In this example, the data is fetched from [Get country](/rest-apis/core-api/countries/get-country) endpoint.

```tsx expandable theme={null}
import { withJsonFormsControlProps } from '@jsonforms/react';
import { rankWith, Resolve, toDataPath } from '@jsonforms/core';
import { useJsonForms } from '@jsonforms/react';
import { Autocomplete, TextField } from '@mui/material';
import { useEffect, useRef, useState } from 'react';

// Custom renderer that fetches subdivisions based on the selected country
const SubdivisionControl = (props) => {
  const { data, path, handleChange, uischema, label } = props;
  const { core } = useJsonForms();
  const [options, setOptions] = useState([]);
  const isFirstRender = useRef(true);
  const { data: dataOptions } = uischema.options || {};

  // Read the country value from the dependency declared in dependsOn
  const dependsOn = uischema.options?.dependsOn;
  const countryScope = dependsOn?.find(({ name }) => name === 'country')?.scope;
  const country = countryScope ? Resolve.data(core?.data, toDataPath(countryScope)) : undefined;

  useEffect(() => {
    if (dataOptions?.source === 'subdivisions' && country) {
      // Clear the subdivision value when the country changes (except on initial render)
      if (!isFirstRender.current) {
        handleChange(path, undefined);
      }
      isFirstRender.current = false;

      // Fetch subdivisions from GET /countries/{country}
      fetch(`https://api.enterprise.uphold.com/core/countries/${country}`, {
        headers: {
          'Authorization': `Bearer ${accessToken}` // Your OAuth2 access token
        }
      })
        .then((res) => res.json())
        .then((data) => {
          const subdivisions = data.country?.subdivisions ?? [];
          setOptions(subdivisions.map((s) => ({ value: s.code, label: s.name })));
        });
    } else {
      setOptions([]);
    }
  }, [dataOptions, country]);

  return (
    <Autocomplete
      options={options}
      value={options.find((o) => o.value === data) || null}
      onChange={(_, selected) => handleChange(path, selected?.value)}
      getOptionLabel={(option) => option.label}
      disabled={!country}
      renderInput={(params) => <TextField {...params} label={label} />}
    />
  );
};

// Tester: use this renderer when data.source is "subdivisions"
const subdivisionTester = rankWith(10, (uischema) =>
  uischema.options?.data?.source === 'subdivisions'
);

// Export for use with JsonForms
export const subdivisionRenderer = {
  tester: subdivisionTester,
  renderer: withJsonFormsControlProps(SubdivisionControl),
};
```

This example demonstrates:

* Reading dependency values from `dependsOn` using `Resolve.data` and `toDataPath`
* Re-fetching subdivisions from [Get country](/rest-apis/core-api/countries/get-country) when the country changes
* Clearing the selected value when the dependency changes (except on initial render)
* Disabling the control until a country is selected

### Date renderer (`format: "date"` + `rules`)

Renders a date picker and converts `rules` (e.g., `difference-greater-than-or-equal-to-threshold`, `difference-less-than-or-equal-to-threshold`) into `min`/`max` date constraints. The `format: "date"` is defined in the JSON Schema; this renderer adds support for the `rules` option from the UI Schema.

```tsx expandable theme={null}
import { withJsonFormsControlProps } from '@jsonforms/react';
import { rankWith } from '@jsonforms/core';
import { TextField } from '@mui/material';

// Custom renderer that handles the format: "date" schema with validation rules
const DateControl = (props) => {
  const { data, path, handleChange, uischema, label } = props;

  const rules = uischema.options?.rules ?? [];

  // Your own function that converts rules into date picker constraints.
  // This must be implemented by the partner (e.g., evaluating threshold rules against the current date).
  const { min, max } = RulesEvaluator.datepicker.evaluate(rules);

  return (
    <TextField
      fullWidth
      margin="normal"
      label={label}
      type="date"
      value={data ?? ''}
      slotProps={{
        inputLabel: { shrink: true },
        htmlInput: { min, max },
      }}
      onChange={(e) => handleChange(path, e.target.value || undefined)}
    />
  );
};

// Tester: use this renderer when schema.format is "date"
const dateTester = rankWith(10, (schema) =>
  schema?.format === 'date'
);

// Export for use with JsonForms
export const dateRenderer = {
  tester: dateTester,
  renderer: withJsonFormsControlProps(DateControl),
};
```

This example demonstrates:

* Detecting `format: "date"` in the JSON Schema with a custom tester
* Delegating rule evaluation to a partner-implemented function
* Applying `min`/`max` constraints to the native date input

### Postal code renderer (`format: "postal-code"` + `dependsOn`)

Renders a text input for postal codes. Uses `dependsOn` to read the current country and subdivision values, then applies country-specific validation patterns.

```tsx expandable theme={null}
import { withJsonFormsControlProps } from '@jsonforms/react';
import { rankWith, Resolve, toDataPath } from '@jsonforms/core';
import { useJsonForms } from '@jsonforms/react';
import { Autocomplete, TextField } from '@mui/material';
import { useEffect, useState } from 'react';

// Custom renderer that validates postal codes based on the selected country
const PostalCodeControl = (props) => {
  const { data, path, handleChange, uischema, label } = props;
  const { core } = useJsonForms();
  const [pattern, setPattern] = useState(null);

  // Read dependency values declared in dependsOn
  const dependsOn = uischema.options?.dependsOn;
  const countryScope = dependsOn?.find(({ name }) => name === 'country')?.scope;
  const subdivisionScope = dependsOn?.find(({ name }) => name === 'subdivision')?.scope;

  const country = countryScope ? Resolve.data(core?.data, toDataPath(countryScope)) : undefined;
  const subdivision = subdivisionScope ? Resolve.data(core?.data, toDataPath(subdivisionScope)) : undefined;

  useEffect(() => {
    if (uischema.options?.format === 'postal-code' && country) {
      // Your own service/function that returns a regex pattern for the given country.
      // This must be implemented by the partner (e.g., from a local mapping or an API).
      PostalCodeService.getPattern(country, subdivision).then(setPattern);
    } else {
      setPattern(null);
    }
  }, [uischema.options?.format, country, subdivision]);

  const validationError = data && pattern && !new RegExp(pattern).test(data) ? 'Invalid postal code format' : undefined;

  return (
    <TextField
      fullWidth
      margin="normal"
      label={label}
      value={data ?? ''}
      disabled={!country}
      error={!!validationError}
      helperText={validationError}
      onChange={(e) => handleChange(path, e.target.value)}
    />
  );
};

// Tester: use this renderer when options.format is "postal-code"
const postalCodeTester = rankWith(10, (uischema) =>
  uischema.options?.format === 'postal-code'
);

// Export for use with JsonForms
export const postalCodeRenderer = {
  tester: postalCodeTester,
  renderer: withJsonFormsControlProps(PostalCodeControl),
};
```

This example demonstrates:

* Reading multiple dependency values (`country`, `subdivision`) from `dependsOn`
* Delegating pattern resolution to a partner-implemented service
* Applying regex validation against the resolved pattern
* Disabling the control until a country is selected

### Registering all custom renderers

Register all custom renderers with the `JsonForms` component:

```tsx expandable theme={null}
import { JsonForms } from '@jsonforms/react';
import { materialRenderers, materialCells } from '@jsonforms/material-renderers';
import { dataSourceRenderer } from './DataSourceControl';
import { subdivisionRenderer } from './SubdivisionControl';
import { dateRenderer } from './DateControl';
import { postalCodeRenderer } from './PostalCodeControl';

const customRenderers = [
  dataSourceRenderer,
  subdivisionRenderer,
  dateRenderer,
  postalCodeRenderer,
  ...materialRenderers,
];

const DynamicForm = ({ schema, uiSchema, data }) => (
  <JsonForms
    schema={schema}
    uischema={uiSchema}
    data={data}
    renderers={customRenderers}
    cells={materialCells}
  />
);
```

## See it in action

To see dynamic forms in practice, explore the KYC processes that use them:

<CardGroup cols={2}>
  <Card title="KYC Processes" icon="user-check" href="/rest-apis/core-api/kyc/introduction">
    Learn how dynamic forms power KYC data collection.
  </Card>
</CardGroup>
