Mastering Formily JSON Schema Rendering: A Deep Dive into Form Rendering Mechanics

This article explains how Formily parses JSON Schema, builds a form tree, handles data binding, validation, and submission, and walks through the internal rendering flow—including createSchemaField, RecursionField, and ReactiveField—while providing practical examples and discussing complex linked‑field scenarios and performance considerations.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
Mastering Formily JSON Schema Rendering: A Deep Dive into Form Rendering Mechanics

Preface

Formily is a powerful form solution based on JSON Schema that offers flexible rendering and data‑binding capabilities. This article explores Formily’s JSON Schema rendering mechanism and implementation details to help you better understand and apply the technology.

Introduction

Formily is a high‑performance form library built on React. It supports three development modes—JSON Schema, JSX Schema, and pure JSX—to describe form structure and validation rules.

JSON Schema is a community‑driven protocol for describing JSON content. It provides rich attributes and constraints that can fully describe each field and its rules, is platform‑agnostic, and offers a compact, readable format compared with JSX.

Basic Concepts

Parsing JSON Schema: Formily parses the incoming JSON Schema object, identifies field types, attributes, and validation rules, selects the appropriate form component, and applies the attributes and rules to it.

const rule = (value) => {
  if (!value?.length) {
    return '请至少新增一个';
  }
  return true;
};

const formSchema = {
  type: 'array',
  title: '明细信息',
  'x-decorator': 'FormItem',
  'x-component': 'ArrayTable',
  'x-validator': rule,
  // ...
};

Building the Form Tree: After parsing, Formily constructs a component tree where each node corresponds to a form field or component.

import { createForm } from '@formily/core';
import { createSchemaField } from '@formily/react-schema-renderer';

const form = createForm();
const SchemaField = createSchemaField({
  components: { FormItem, Input, Password, VerifyCode, /* ... */ },
});

const formTree = (
  <Form form={form}>
    <SchemaField schema={formSchema} />
  </Form>
);

Form Data Binding: Formily uses two‑way binding to keep form data and component values synchronized.

Form Validation: Validation rules defined in JSON Schema are automatically applied, providing real‑time feedback.

Form Submission: After completion, Formily’s API can retrieve form data for server submission or other business logic.

Rendering Process

During development we usually declare a SchemaField with createSchemaField to load a Schema. The core implementation looks like this:

export function createSchemaField(options) {
  if (options === void 0) { options = {}; }
  function SchemaField(props) {
    var schema = Schema.isSchemaInstance(props.schema)
      ? props.schema
      : new Schema({ type: 'object', ...props.schema });
    var renderMarkup = function () {
      env.nonameId = 0;
      if (props.schema) return null;
      return render(
        React.createElement(SchemaMarkupContext.Provider, { value: schema }, props.children)
      );
    };
    var renderChildren = function () {
      return React.createElement(RecursionField, { ...props, schema });
    };
    return (
      React.createElement(SchemaOptionsContext.Provider, { value: options },
        React.createElement(SchemaComponentsContext.Provider, { value: lazyMerge(options.components, props.components) },
          React.createElement(ExpressionScope, { value: lazyMerge(options.scope, props.scope) },
            renderMarkup(),
            renderChildren()))));
  }
  SchemaField.displayName = 'SchemaField';
  return SchemaField;
}

The code ultimately reaches the renderChildren method, which renders RecursionField:

export var RecursionField = function (props) {
  var basePath = useBasePath(props);
  var fieldSchema = useMemo(() => new Schema(props.schema), [props.schema]);
  var fieldProps = useFieldProps(fieldSchema);
  var renderProperties = function (field) {
    if (props.onlyRenderSelf) return;
    var properties = Schema.getOrderProperties(fieldSchema);
    if (!properties.length) return;
    return (
      React.createElement(Fragment, null,
        properties.map(({ schema: item, key: name }, index) => {
          var base = (field ?? field.address) || basePath;
          var schema = item;
          if (isFn(props.mapProperties)) {
            var mapped = props.mapProperties(item, name);
            if (mapped) schema = mapped;
          }
          if (isFn(props.filterProperties) && props.filterProperties(schema, name) === false) {
            return null;
          }
          return React.createElement(RecursionField, {
            schema,
            key: `${index}-${name}`,
            name,
            basePath: base,
          });
        })
      )
    );
  };
  var render = function () {
    if (!isValid(props.name)) return renderProperties();
    if (fieldSchema.type === 'object') {
      if (props.onlyRenderProperties) return renderProperties();
      return React.createElement(ObjectField, { ...fieldProps, name: props.name, basePath }, renderProperties);
    } else if (fieldSchema.type === 'array') {
      return React.createElement(ArrayField, { ...fieldProps, name: props.name, basePath });
    } else if (fieldSchema.type === 'void') {
      if (props.onlyRenderProperties) return renderProperties();
      return React.createElement(VoidField, { ...fieldProps, name: props.name, basePath }, renderProperties);
    }
    return React.createElement(Field, { ...fieldProps, name: props.name, basePath });
  };
  if (!fieldSchema) return React.createElement(Fragment, null);
  return React.createElement(SchemaContext.Provider, { value: fieldSchema }, render());
};

The key points are:

At line 26 the RecursionField calls itself recursively, which explains how Formily traverses properties and renders them.

The render method selects different Field components based on the schema type.

All field components eventually render a ReactiveField layer. Here is the core implementation:

var ReactiveInternal = function (props) {
  var components = useContext(SchemaComponentsContext);
  if (!props.field) {
    return React.createElement(Fragment, null, renderChildren(props.children));
  }
  var field = props.field;
  var content = mergeChildren(
    renderChildren(props.children, field, field.form),
    field.content ?? field.componentProps.children
  );
  if (field.display !== 'visible') return null;
  var getComponent = target => isValidComponent(target) ? target : FormPath.getIn(components, target) || target;
  var renderDecorator = children => {
    if (!field.decoratorType) return React.createElement(Fragment, null, children);
    return React.createElement(getComponent(field.decoratorType), toJS(field.decoratorProps), children);
  };
  var renderComponent = () => {
    if (!field.componentType) return content;
    var value = !isVoidField(field) ? field.value : undefined;
    var onChange = !isVoidField(field) ? (...args) => { field.onInput(...args); field.componentProps?.onChange?.(...args); } : field.componentProps?.onChange;
    var onFocus = !isVoidField(field) ? (...args) => { field.onFocus(...args); field.componentProps?.onFocus?.(...args); } : field.componentProps?.onFocus;
    var onBlur = !isVoidField(field) ? (...args) => { field.onBlur(...args); field.componentProps?.onBlur?.(...args); } : field.componentProps?.onBlur;
    var disabled = !isVoidField(field) ? field.pattern === 'disabled' || field.pattern === 'readPretty' : undefined;
    var readOnly = !isVoidField(field) ? field.pattern === 'readOnly' : undefined;
    return React.createElement(
      getComponent(field.componentType),
      { ...toJS(field.componentProps), disabled, readOnly, value, onChange, onFocus, onBlur },
      content
    );
  };
  return renderDecorator(renderComponent());
};
ReactiveInternal.displayName = 'ReactiveField';
export var ReactiveField = observer(ReactiveInternal, { forwardRef: true });

The ReactiveField layer provides the fine‑grained reactivity that allows Formily to refresh only the components whose data actually changed.

Practice

Simple form construction:

{
  name: {
    title: '姓名',
    type: 'string',
    'x-decorator': 'FormItem',
    'x-component': 'Input',
    'x-component-props': { placeholder: '请输入姓名' },
  },
  age: {
    title: '年龄',
    type: 'number',
    'x-decorator': 'FormItem',
    'x-component': 'Input',
    'x-component-props': { placeholder: '请输入年龄' },
  },
  gender: {
    title: '性别',
    type: 'number',
    'x-decorator': 'FormItem',
    'x-component': 'Radio.Group',
    enum: [
      { label: '男', value: 1 },
      { label: '女', value: 0 },
    ],
  },
}

Resulting UI was too wide, so we wrapped the fields with an inline layout:

{
  layout: {
    type: 'void',
    'x-component': 'Layout.Inline',
    'x-decorator': 'FormGrid',
    properties: {
      name: { /* same as above */ },
      age: { /* same as above */ },
      gender: { /* same as above */ },
    },
  },
}

Later the backend required nesting the name and age under an obj object and renaming name to nickname. The schema was updated accordingly, and the form data matched the new structure.

We also added a conditional rule: when gender is female, the age field becomes disabled:

{
  age: {
    /* ... */
    'x-reactions': {
      dependencies: ['gender'],
      fulfill: {
        schema: { 'x-disabled': '{{ $deps[0] === 0 }}' },
      },
    },
  },
}

Complex Scenario

Requirement Overview

Two linked select fields—"Business Object Code" and "Business Object Name"—must fetch data from an API when either changes and populate three other fields (owner, type, etc.).

Analysis

Using x-reactions alone cannot perform the API call, so a custom component that encapsulates the request logic is needed.

const BusinessObjectSelect: React.FunctionComponent<IBusinessObjectSelectProps> = ({ params, onChange }) => {
  const handleChange = () => {
    const requestParams = transformParams(params);
    const { response } = service.fetchBusinessObjectData(requestParams);
    const data = transformResponse(response);
    onChange(data);
  };
  return (
    <Select
      {...props}
      labelInValue
      value={value}
      onSearch={handleSearch}
      onChange={handleChange}
      options={data}
      defaultActiveFirstOption={false}
      showArrow={false}
      filterOption={false}
    />
  );
};

After registering the component, the schema’s x-component values are replaced with the custom component, and x-reactions are adjusted to depend on the other select’s value.

Challenges encountered:

Choosing the correct dependency for the remaining two fields (ownerName, businessObjectType) so that changes in either select trigger updates.

Form data became polluted because the select’s onChange returned an object containing the whole form instead of a simple value.

Dependencies needed to be objects for the reactions, requiring extra conversion during form initialization.

The solution felt unintuitive and added hidden complexity.

Maintenance cost grew as schema adjustments required many small changes.

Possible fixes include manually triggering onChange after value changes, adding a data‑conversion layer, or using createForm effects to listen for changes and update the form programmatically.

Conclusion

Relying solely on Formily’s built‑in linking can be limiting for complex scenarios. Repeated JSON Schema parsing may become a performance bottleneck for large schemas with extensive reactions, as Formily performs deep copies and dirty checks. In such cases, limiting local re‑renders or falling back to full‑tree React rendering might be considered.

Ultimately, a solution that covers 80% of cases is acceptable; the remaining edge cases often require a different approach.

Final Note

📚 Recommended articles from the author:

How Guming Built a Front‑End Data Center

Two Ways to Invoke a React Component

Backend Business Development (Part 1): Form Principles

Follow the "Goodme Front‑End Team" public account for more practical insights.

JSON SchemaReActForm Rendering
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.