DEV3 Technologies home

JSON form schema to validation rules

Turn a stored list of field definitions into Laravel validation rules and attribute names, so user-built forms are validated server side.

LaravelFormsvalidationform-builderjsonrules

Code

<?php

namespace App\Forms;

use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

/**
 * Field shape: {"key":"email","label":"Work email","type":"email","required":true,
 *               "options":["a","b"],"min":1,"max":5}
 */
final class SchemaRules
{
    /** @param  list<array<string,mixed>>  $fields */
    public static function rules(array $fields): array
    {
        $rules = [];

        foreach ($fields as $f) {
            $key = 'answers.'.$f['key'];
            $base = ($f['required'] ?? false) ? ['required'] : ['nullable'];

            $type = match ($f['type']) {
                'text' => ['string', 'max:'.($f['max'] ?? 255)],
                'textarea' => ['string', 'max:'.($f['max'] ?? 5000)],
                'email' => ['email:rfc', 'max:255'],
                'url' => ['url', 'max:2048'],
                'number' => ['numeric', ...self::bounds($f)],
                'date' => ['date'],
                'checkbox' => ['boolean'],
                'select', 'radio' => ['string', Rule::in($f['options'] ?? [])],
                'multiselect' => ['array', ...self::bounds($f)],
                default => ['prohibited'], // unknown types can never be submitted
            };

            $rules[$key] = [...$base, ...$type];

            if ($f['type'] === 'multiselect') {
                $rules[$key.'.*'] = ['string', Rule::in($f['options'] ?? [])];
            }
        }

        return $rules;
    }

    public static function attributes(array $fields): array
    {
        return collect($fields)->mapWithKeys(fn ($f) => ['answers.'.$f['key'] => $f['label'] ?? $f['key']])->all();
    }

    /** Validate a submission and return only the answers the schema knows about. */
    public static function validate(array $fields, array $input): array
    {
        $v = Validator::make($input, self::rules($fields), [], self::attributes($fields));

        return $v->validate()['answers'] ?? [];
    }

    private static function bounds(array $f): array
    {
        return array_filter([
            isset($f['min']) ? 'min:'.$f['min'] : null,
            isset($f['max']) ? 'max:'.$f['max'] : null,
        ]);
    }
}

How it works

What it solves

When customers build their own forms, the fields live in a JSON column rather than in a FormRequest. You still need real server-side validation for every submission. This class translates the stored field list into the same rule arrays you would write by hand.

How it works

Each field has a key, a label, a type, an optional required flag and optional options, min and max. rules() maps every field to dotted rules under answers.<key>. A match expression picks the type rules: strings get a length cap, emails use RFC checking, selects and radios are limited with Rule::in() to the configured options, and multiselects validate the array plus every item with a .* rule.

Anything with an unknown type gets prohibited, so a tampered schema or payload cannot smuggle extra values. attributes() maps keys to labels so error messages read "Work email is required" instead of "answers.email". validate() runs the validator and returns only the validated answers, which drops unexpected keys.

Adapting it

  • Add types such as phone or rating as new arms of the match.
  • Add conditional logic by wrapping a field's rules with required_if.
  • Cache the compiled rules per form version if forms are large.

Pitfalls

  • Never trust field keys from the browser; they must come from the saved schema.
  • Reject keys containing dots when saving the schema, because dots change meaning in rule names.

How to use it

  1. Store your fields as a JSON array on the form model (cast to array).
  2. Put SchemaRules.php in app/Forms/.
  3. In the public submit controller: $answers = SchemaRules::validate($form->fields, $request->all());
  4. Save $answers as JSON next to the response record.
  5. Validation errors are thrown as a normal ValidationException, keyed by answers.<key>.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets