Skip to content

Schema Composition

As forms grow, schema definitions tend to become large and difficult to maintain.

A single form often contains validation rules, conditional logic, readonly states, feature-specific requirements and UI state rules.

Keeping all of this inside one schema function quickly leads to tightly coupled form logic.

export const ContactSchema = schema<ContactForm>((path) => {
requiredTrimmed(path.name);
requiredTrimmed(path.email);
requiredTrimmed(path.companyName);
disabledHidden(
path.companyName,
valueEquals(path.personType, 'private')
);
readonly(path.email, () => isReadonly());
});

This works, but it does not scale well.

Common problems are duplicated logic, unclear ownership, difficult testing and poor reuse.

Instead of defining all rules in one place, schemas can be split into focused fragments.

const BaseContactSchema = schema<ContactForm>((path) => {
requiredTrimmed(path.name);
requiredTrimmed(path.email);
});
const CompanyContactSchema = schema<ContactForm>((path) => {
requiredTrimmed(path.companyName);
});
export const ContactSchema = append(
BaseContactSchema,
CompanyContactSchema
);

Composition keeps different concerns separate.

A base schema can define the rules that are always valid.

A feature schema can add rules for a specific use case.

A workflow schema can add readonly or disabled states.

export const EnterpriseContactSchema = append(
BaseContactSchema,
CompanyContactSchema,
EnterpriseRules
);

You do not always need to create a separate schema file.

append also accepts inline schema functions.

export const ContactSchema = append(
BaseContactSchema,
(path) => {
disabledHidden(
path.companyName,
valueEquals(path.personType, 'private')
);
}
);

This is useful for small local extensions.

Schema composition avoids deep inheritance-like structures.

Instead of thinking in terms of this model:

EnterpriseForm extends BaseForm

think in terms of this model:

EnterpriseForm =
BaseSchema
+ CompanyRules
+ WorkflowRules

The final schema is assembled from small, explicit building blocks.

Schema composition is useful for large enterprise forms, multi-step workflows, feature-dependent forms, white-label applications, readonly modes and dynamic onboarding flows.

A good rule of thumb is:

One schema fragment should own one concern.

For example:

BaseContactSchema
CompanyContactSchema
ReadonlySchema
CountrySpecificSchema

These fragments can then be combined as needed.

export const FinalContactSchema = append(
BaseContactSchema,
CompanyContactSchema,
CountrySpecificSchema
);

This keeps validation logic modular, maintainable and easier to change over time.