How to generate a json schema from a form submission object
- Step 1Build a complete sample submission — Write a JSON object with every form field filled:
{ "firstName": "Alice", "email": "alice@x.com", "age": 28, "newsletterOptIn": true }. Avoidnullfor optional fields in the sample — a null infers"type":"null", which rejects real input. - Step 2Load the submission JSON — Drop the
.jsonfile (free tier up to 2 MB) or paste it. The tool runs strictJSON.parseon one value — no trailing commas, comments, or line-delimited submissions. - Step 3Name the schema — Set Schema title (default
MySchema) to the form name, e.g.SignupForm. It's written to the roottitleonly and documents the schema; it doesn't change validation. - Step 4Set required and additionalProperties — Keep Mark all fields required on so every field is in
required— relax later. Allow additional properties off (additionalProperties: false) makes AJV reject stray fields like an injected hidden input. - Step 5Generate and download the base schema — Click Generate Schema, then Download Schema (
<name>.schema.json, 2-space indent) or Copy. The base captures field names and types — now refine it. - Step 6Add field rules and wire the resolver — Add
format: email,minLength,pattern, numeric bounds, andenumper field; convert null fields to nullable unions; prune optional fields fromrequired. ThenuseForm({ resolver: ajvResolver(schema) }).
Form field → inferred base vs the rule you must add
The generator infers the type; you add the validation rule. AJV (via ajvResolver) reports each as a field-level error.
| Field | Sample value | Inferred base | Rule to add by hand |
|---|---|---|---|
"alice@x.com" | {"type":"string"} | "format":"email" (with ajv-formats) | |
| age | 28 | {"type":"integer"} | "minimum":0, "maximum":120 |
| password | "hunter2" | {"type":"string"} | "minLength":8, "pattern":"..." |
| country | "US" | {"type":"string"} | "enum":["US","CA","GB"] |
| newsletterOptIn | true | {"type":"boolean"} | usually none |
| middleName | null | {"type":"null"} | change to ["string","null"] then drop from required |
Validation surface and how the schema is used
The same generated schema works across these layers. Library-specific wiring shown.
| Surface | How the schema plugs in | Errors |
|---|---|---|
| React Hook Form | useForm({ resolver: ajvResolver(schema) }) | Per-field formState.errors |
| Formik | custom validate via AJV → map errors to fields | Field-level error objects |
| Server-side (Node) | new Ajv().compile(schema) in the handler | validate.errors array |
| Shared client+server | one schema file imported in both | Identical rules both sides |
Cookbook
A signup form from sample submission to a wired ajvResolver — including adding the email/length/enum rules and fixing an optional field the generator over-constrained.
Signup submission to a base schema
ExampleA complete sample produces a base with all fields required and types inferred. This is the starting point.
Sample: { "firstName":"Alice", "email":"alice@x.com", "age":28, "newsletterOptIn":true }
Schema title: SignupForm · required: ON
Generated base:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SignupForm",
"type": "object",
"properties": {
"firstName": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer" },
"newsletterOptIn": { "type": "boolean" }
},
"required": ["firstName","email","age","newsletterOptIn"],
"additionalProperties": false
}Add the field rules a real form needs
ExampleRefine the base: email format, name length, age bounds. These produce the per-field error messages users see.
Refined properties:
"firstName": { "type": "string", "minLength": 1, "maxLength": 50 },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 13, "maximum": 120 },
"newsletterOptIn": { "type": "boolean" }
// 'format: email' needs ajv-formats registered on the Ajv instance.Wire into React Hook Form
ExamplePass the refined schema to ajvResolver. Validation errors appear in formState.errors keyed by field name.
import { useForm } from 'react-hook-form';
import { ajvResolver } from '@hookform/resolvers/ajv';
import schema from './SignupForm.schema.json';
const { register, handleSubmit, formState: { errors } } =
useForm({ resolver: ajvResolver(schema) });
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}Fix an optional field the generator over-constrained
ExampleAn empty optional field submitted as null infers type:null and lands in required. Make it nullable and optional.
Sample had: "middleName": null
Generated: "middleName": { "type": "null" } AND "middleName" in required
Fix:
- property: { "type": ["string", "null"] }
- remove "middleName" from the required array
Now the field is optional and accepts a string or empty/null.Add a select/enum constraint
ExampleA country select infers a plain string. Add an enum so AJV rejects values outside the option list.
Generated: "country": { "type": "string" }
Refined: "country": { "type": "string", "enum": ["US", "CA", "GB"] }
AJV error if a tampered form posts country: 'ZZ':
{ keyword: 'enum', instancePath: '/country',
message: 'must be equal to one of the allowed values' }Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
An empty optional field submitted as null becomes type:null
By designIf your sample submission has middleName: null for an empty optional field, the generator infers { "type": "null" } and (with required on) lists it in required. AJV then rejects any real entry. Fix it twice: change the property to "type": ["string", "null"] and remove it from required. Better still, omit empty optional fields from the sample.
format, minLength, pattern, and enum are not inferred
Not inferredAn email field infers "type":"string", not format:"email"; a password is a string with no minLength; a select is a string with no enum. These are the rules a real form needs and you must add them by hand. Without them AJV only checks types — "notanemail" passes.
An integer field rejects decimal input
Strict typeage: 28 infers "type":"integer", so AJV rejects 28.5. Usually correct for age, but for a field that accepts decimals (e.g. a rating) change the type to "number". If you want to accept numeric strings from an input, add AJV coercion (coerceTypes: true) since HTML inputs submit strings.
HTML inputs submit strings, but the schema expects number/boolean
Type mismatchA native <input type="number"> still submits a string, and a checkbox can submit "on". The schema generated from a typed sample expects integer/boolean. Either coerce in the form layer (React Hook Form valueAsNumber), or enable AJV coerceTypes: true so "28" validates against integer.
Checkbox-group array of objects keeps only the first item's shape
Partial schemaIf a field is an array of objects (e.g. repeated address rows), item schemas merge by top-level type and only the FIRST element's keys survive in items. Validation then ignores keys present only on later rows. Generate from a sample whose first array element is complete, or hand-edit items.properties.
additionalProperties:false only on the root
Root onlyTurning off Allow additional properties rejects stray top-level fields (good for blocking injected hidden inputs), but it is written on the root only. Nested object fields (e.g. an address group) accept unknown keys. Add additionalProperties: false to each nested object manually.
Conditional rules (require X only when Y) are not generated
Not inferredThe generator produces a flat type schema with no if/then/else. For 'require phone only when contactMethod is phone', add an if/then block by hand after generating. AJV evaluates these conditionals at validation time; the base schema is your starting point.
Submission sample over the free 2 MB limit
Schema limitFree JSON processing caps at 2,000,000 bytes (2 MB). A single form submission is tiny, so you'll rarely hit this — but a sample padded with a huge file-upload data URL could. Strip large embedded blobs from the sample. Pro raises the cap to 100 MB.
Frequently asked questions
How do I wire the schema into React Hook Form?
Install @hookform/resolvers and ajv (plus ajv-formats if you use format). Then useForm({ resolver: ajvResolver(schema) }). Validation errors appear in formState.errors keyed by field name, ready for inline messages.
Why doesn't my email field reject 'notanemail'?
The generator infers "type":"string", not format:"email" — it never reads values to guess formats. Add "format":"email" to the property and register ajv-formats on the Ajv instance so AJV actually checks the format.
My optional field is being marked required — why?
With Mark all fields required on, every key in the sample is added to required. Remove the optional keys from the required array. If the field was null in the sample it also got "type":"null" — change it to ["string","null"] too.
How do I validate a number input that submits a string?
HTML inputs submit strings. Either convert in the form layer (register('age', { valueAsNumber: true })) or compile AJV with coerceTypes: true so "28" validates against an integer schema.
Can I add conditional validation like require-if?
Yes, by hand. The generator produces a flat schema with no conditionals. Add an if/then/else block — e.g. if contactMethod equals phone, then required: [phone]. AJV evaluates it at validation time.
Should I use JSON Schema or Zod for my form?
JSON Schema (via ajvResolver) wins when you share one schema between client and server, or generate it from an OpenAPI spec. If you're pure TypeScript and want type inference from the validator, the JSON to Zod tool emits a Zod schema you can use with zodResolver.
How do I constrain a select to its options?
Add an enum to the field by hand — { "type": "string", "enum": ["US","CA","GB"] }. The generator never infers enums, so a select field is a plain string until you add the allowed values.
Does additionalProperties:false block injected fields?
On the root, yes — it makes AJV reject unexpected top-level fields, including injected hidden inputs. It's written on the root only, so add it to each nested object group manually if those need the same protection.
Can I change the output indentation?
Not from the UI — output is always 2-space indented. Reformat the file with the JSON prettifier or JSON minifier if you need a different layout.
Is the submission data uploaded anywhere?
No. Generation is 100% client-side. Form submission objects — including any user-entered personal data in your sample — never reach JAD Apps servers.
Why did my sample fail to load?
Parsing is strict JSON.parse. Trailing commas, comments, and line-delimited submissions throw. Clean the sample with the JSON validator and generate from a single JSON object.
What's the largest sample I can use for free?
2 MB (2,000,000 bytes) per file, one at a time. A form submission is normally well under that unless it embeds large file-upload blobs. Pro raises the limit to 100 MB and 10 files.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.