Validation
Validate form inputs with built-in and custom functions.
Built-in Validators
json-render includes common validation functions:
required— Value must be non-emptyemail— Valid email formatminLength— Minimum string length (args:{ "min": N })maxLength— Maximum string length (args:{ "max": N })pattern— Match a regex pattern (args:{ "pattern": "regex" })min— Minimum numeric value (args:{ "min": N })max— Maximum numeric value (args:{ "max": N })numeric— Value must be a numberurl— Valid URL formatmatches— Must equal another field (args:{ "other": { "$state": "/path" } })equalTo— Alias for matches (args:{ "other": { "$state": "/path" } })lessThan— Value must be less than another field (args:{ "other": { "$state": "/path" } })greaterThan— Value must be greater than another field (args:{ "other": { "$state": "/path" } })requiredIf— Required only when another field is truthy (args:{ "field": { "$state": "/path" } })
Using Validation in JSON
Use { "$bindState": "/path" } on the value prop for two-way binding. Validation checks run against the value at the bound path (available as bindings?.value in components):
{
"type": "TextField",
"props": {
"label": "Email",
"value": { "$bindState": "/form/email" },
"checks": [
{ "type": "required", "message": "Email is required" },
{ "type": "email", "message": "Invalid email format" }
],
"validateOn": "blur"
}
}Validation with Parameters
{
"type": "TextField",
"props": {
"label": "Password",
"value": { "$bindState": "/form/password" },
"checks": [
{ "type": "required", "message": "Password is required" },
{
"type": "minLength",
"args": { "min": 8 },
"message": "Password must be at least 8 characters"
},
{
"type": "pattern",
"args": { "pattern": "[A-Z]" },
"message": "Must contain at least one uppercase letter"
}
]
}
}Custom Validation Functions
Define custom validators in your catalog's functions field. The catalog itself is framework-agnostic — only the schema import varies by platform:
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react/schema'; // or '@json-render/react-native/schema'
import { z } from 'zod';
const catalog = defineCatalog(schema, {
components: { /* ... */ },
functions: {
isValidPhone: {
description: 'Validates phone number format',
},
isUniqueEmail: {
description: 'Checks if email is not already registered',
},
},
});Usage with React
In @json-render/react, use ValidationProvider to supply implementations for your custom validators:
import { ValidationProvider } from '@json-render/react';
function App() {
const customValidators = {
isValidPhone: (value) => {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
return phoneRegex.test(value);
},
isUniqueEmail: async (value) => {
const response = await fetch(`/api/check-email?email=${value}`);
const { available } = await response.json();
return available;
},
};
return (
<ValidationProvider customFunctions={customValidators}>
{/* Your UI */}
</ValidationProvider>
);
}Using in Components
The useFieldValidation and useBoundProp hooks wire validation into your registry components. Validation uses the path from bindings?.value (the bound state path):
import { useFieldValidation, useBoundProp } from '@json-render/react';
function TextField({ props, bindings }) {
const [value, setValue] = useBoundProp(props.value, bindings?.value);
const { errors, isValid, validate, touch, clear } = useFieldValidation(
bindings?.value ?? null,
{ checks: props.checks, validateOn: props.validateOn }
);
return (
<div>
<label>{props.label}</label>
<input
value={value || ''}
onChange={(e) => setValue(e.target.value)}
onBlur={() => validate()}
/>
{errors.map((error, i) => (
<p key={i} className="text-red-500 text-sm">{error}</p>
))}
</div>
);
}See the @json-render/react API reference for full ValidationProvider and useFieldValidation documentation.
Cross-Field Validation
Validation args support { "$state": "/path" } references to compare against other fields. This enables cross-field rules like "confirm password must match password":
{
"type": "Input",
"props": {
"label": "Confirm Password",
"value": { "$bindState": "/form/confirmPassword" },
"checks": [
{ "type": "required", "message": "Please confirm your password" },
{
"type": "matches",
"args": { "other": { "$state": "/form/password" } },
"message": "Passwords must match"
}
]
}
}Other cross-field examples:
{
"checks": [
{
"type": "greaterThan",
"args": { "other": { "$state": "/form/startDate" } },
"message": "End date must be after start date"
}
]
}{
"checks": [
{
"type": "requiredIf",
"args": { "field": { "$state": "/form/enableNotifications" } },
"message": "Email is required when notifications are enabled"
}
]
}Conditional Validation
Use the enabled field in the validation config to only run checks when a condition is met:
{
"type": "Input",
"props": {
"label": "Company Name",
"value": { "$bindState": "/form/company" },
"checks": [
{ "type": "required", "message": "Company name is required" }
]
}
}In the component implementation, you can pass enabled to useFieldValidation:
useFieldValidation(bindings?.value ?? "", {
checks: props.checks ?? [],
enabled: { "$state": "/form/accountType", eq: "business" },
});This only validates the company name when the account type is "business".
Validation Timing
Control when validation runs with validateOn:
change— Validate on every input changeblur— Validate when field loses focus (default for Input, Textarea)submit— Validate only on form submission
Form-Level Validation
Use the built-in validateForm action to validate all registered fields at once. This is useful for a "Submit" button that should validate the entire form before proceeding:
{
"type": "Button",
"props": { "label": "Submit" },
"on": {
"press": [
{ "action": "validateForm", "params": { "statePath": "/formResult" } },
{ "action": "submitForm" }
]
},
"children": []
}The validateForm action runs validateAll() and writes { valid: boolean } to the specified state path (defaults to /formValidation). Your submit handler can then check { "$state": "/formResult/valid" } to decide whether to proceed.
Note: Actions in a list execute sequentially, but
submitFormdoes not automatically gate on validation. Guard submission with a$condvisibility condition on the button or check{ "$state": "/formResult/valid" }inside your action handler to skip submission when the form is invalid.
Next
- Computed Values — derive dynamic prop values
- Watchers — react to state changes
- Generation Modes — how AI generates specs