DEV Community

Cover image for Stop Writing Validation Code. Start Writing JSON
Jean Duppont
Jean Duppont

Posted on

Stop Writing Validation Code. Start Writing JSON

The Problem Every Developer Faces

You're building a form. Simple registration form. Email, password, done.

// Joi
const schema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});
Enter fullscreen mode Exit fullscreen mode

Two weeks later:

"Hey, can we make password 12 characters for enterprise users?"

You: Opens IDE, modifies code, writes tests, creates PR, waits for review, deploys

Product Manager: "Actually, can we A/B test 10 vs 12 characters first?"

You: Screams internally


What If Validation Wasn't Code?
What if validation rules were just... data?

[
  {
    "name": "email",
    "validation": [
      { "rule": "required" },
      { "rule": "email" }
    ]
  },
  {
    "name": "password",
    "validation": [
      { "rule": "required" },
      { "rule": "minLength", "value": 8 }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

That's it. Pure JSON.

No imports. No code. No deployment.


Introducing: Template-Based Validation

import { Validator } from '@teonord/validator';

// Your data
const formData = {
  email: 'user@example.com',
  password: 'pass123'
};

// Your validation rules (can be from anywhere)
const template = await fetch('/api/validation-rules').then(r => r.json());

// Validate
const result = new Validator(formData).validateTemplate(template);

if (!result.isValid) {
  console.log(result.errors);
}
Enter fullscreen mode Exit fullscreen mode

Notice what just happened:

✅ Validation rules fetched from API
✅ Zero code changes needed
✅ Rules can change without deployment
✅ Non-developers can modify rules


Real-World Game Changers

1. Multi-Tenant SaaS

Different customers, different rules.

// Enterprise customer: strict validation
const enterpriseTemplate = [
  {
    name: "password",
    validation: [
      { rule: "minLength", value: 16 },
      { rule: "alphaNumeric" }
    ]
  }
];

// Startup customer: relaxed validation
const startupTemplate = [
  {
    name: "password",
    validation: [
      { rule: "minLength", value: 8 }
    ]
  }
];

// Load based on tenant
const template = await getValidationTemplate(tenantId);
const result = new Validator(data).validateTemplate(template);
Enter fullscreen mode Exit fullscreen mode

No code changes. No deployments. Just configuration.


2. Compliance & Regulations

GDPR in EU. Different rules in California. Different rules in Brazil.

// Load rules based on user location
const template = await getValidationForRegion(userCountry);

// EU template includes GDPR-specific validations
// California template includes CCPA requirements
// All without touching code
Enter fullscreen mode Exit fullscreen mode

Compliance team updates rules → Changes go live immediately

3. A/B Testing Validation

Product wants to test if stricter validation improves data quality.

const template = experimentGroup === 'strict'
  ? await fetch('/templates/strict-validation.json')
  : await fetch('/templates/standard-validation.json');

const result = new Validator(data).validateTemplate(template);
Enter fullscreen mode Exit fullscreen mode

Run experiments on validation rules. Measure impact. Iterate.
No engineering time wasted.

4. CMS-Driven Forms

Build an admin panel where product team configures validation.

// Admin creates form in CMS
// Defines validation rules via UI
// Rules saved as JSON in database

// Frontend fetches and applies
const formConfig = await api.getForm(formId);
const result = new Validator(formData).validateTemplate(formConfig.validation);
Enter fullscreen mode Exit fullscreen mode

Product team owns validation. Engineering team builds once.

Built-In Internationalization

Every rule supports multiple languages out of the box.

{
  "name": "email",
  "validation": [
    {
      "rule": "required",
      "message": {
        "en": "Email is required",
        "es": "El email es requerido",
        "fr": "L'email est requis",
        "de": "E-Mail ist erforderlich"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

One template. Multiple languages. Zero code duplication.

Nested Object Validation

Dot notation for deep validation.

[
  {
    "name": "user.profile.email",
    "validation": [
      { "rule": "email" }
    ]
  },
  {
    "name": "user.address.zipCode",
    "validation": [
      { "rule": "regex", "value": "^[0-9]{5}$" }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Clean. Readable. No complex nesting.

Conditional Validation

Validate based on other field values.

[
  {
    "name": "payment_method",
    "validation": [
      { "rule": "required" },
      { "rule": "in", "value": ["credit_card", "paypal"] }
    ]
  },
  {
    "name": "credit_card_number",
    "validation": [
      {
        "rule": "requiredIf",
        "value": ["payment_method", "credit_card"]
      },
      {
        "rule": "minLength",
        "value": 16
      }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Dynamic validation logic. Still just JSON.

Version Control for Validation

Track validation changes like any other data.

CREATE TABLE validation_templates (
  id UUID PRIMARY KEY,
  name VARCHAR(255),
  template JSONB,
  version INTEGER,
  created_at TIMESTAMP,
  created_by VARCHAR(255)
);
Enter fullscreen mode Exit fullscreen mode

Audit trail:
Who changed validation rules?
When?
What changed?
Rollback to previous version instantly

The Architecture

┌──────────────────┐
│   Admin Panel    │ ← Product team configures rules
└────────┬─────────┘
         │
         ↓
┌──────────────────┐
│    Database      │ ← Store templates as JSON
└────────┬─────────┘
         │
         ↓
┌──────────────────┐
│   API Endpoint   │ ← Serve templates
└────────┬─────────┘
         │
         ↓
┌──────────────────┐
│  Frontend/API    │ ← Fetch & validate
└──────────────────┘
Enter fullscreen mode Exit fullscreen mode

Separation of concerns:
Business logic (validation rules) = Configuration
Application logic (how to validate) = Code

Comparison: The Old Way vs The New Way

Changing Password Requirements

Old Way (Joi/Zod):

  1. Open IDE
  2. Find validation file
  3. Modify code
  4. Write tests
  5. Create PR
  6. Code review
  7. Merge
  8. Deploy
  9. Monitor Time: 2-3 days People involved: 2-3 developers Risk: High (code changes)

New Way (@teonord/validator):

  1. Update JSON in database
  2. Done Time: 2 minutes People involved: 1 product manager Risk: Low (configuration change, instant rollback)

Real Production Example

// Lambda function
export const handler = async (event) => {
  const data = JSON.parse(event.body);

  // Load validation template from DynamoDB
  const template = await dynamodb.get({
    TableName: 'ValidationTemplates',
    Key: { formId: event.pathParameters.formId }
  });

  // Validate
  const result = new Validator(data).validateTemplate(template.rules);

  if (!result.isValid) {
    return {
      statusCode: 400,
      body: JSON.stringify({ errors: result.errors })
    };
  }

  // Process valid data
  await processForm(data);

  return {
    statusCode: 200,
    body: JSON.stringify({ success: true })
  };
};
Enter fullscreen mode Exit fullscreen mode

Update validation rules → No deployment needed

Why This Matters

Traditional validation libraries:

  • Validation = Code
  • Changes require deployment
  • Developers are bottleneck
  • Hard to A/B test
  • No runtime flexibility

@teonord/validator templates:

  • Validation = Configuration
  • Changes are instant
  • Product team can iterate
  • Easy to experiment
  • Complete runtime flexibility

The Paradigm Shift

Stop thinking: "I need to write validation code"
Start thinking: "I need to configure validation rules"

Getting Started

npm install @teonord/validator
Enter fullscreen mode Exit fullscreen mode

Create your first template:

[
  {
    "name": "email",
    "validation": [
      {
        "rule": "required",
        "message": { "en": "Email is required" }
      },
      {
        "rule": "email",
        "message": { "en": "Invalid email format" }
      }
    ]
  }
]
Enter fullscreen mode Exit fullscreen mode

Use it:

import { Validator } from '@teonord/validator';

const template = require('./validation-template.json');
const result = new Validator(formData).validateTemplate(template);
Enter fullscreen mode Exit fullscreen mode

That's it.

Advanced: Dynamic Rule Loading

class ValidationService {
  async validate(formId, data) {
    // Load from cache first
    let template = await redis.get(`validation:${formId}`);

    if (!template) {
      // Load from database
      template = await db.getValidationTemplate(formId);

      // Cache for 1 hour
      await redis.set(`validation:${formId}`, template, 'EX', 3600);
    }

    return new Validator(data).validateTemplate(template);
  }
}
Enter fullscreen mode Exit fullscreen mode

Fast. Flexible. Cacheable.

Who Should Use This?

✅ SaaS platforms with multiple tenants
✅ Enterprise apps with complex compliance
✅ International products needing multi-language validation
✅ Rapid iteration teams doing A/B testing
✅ Low-code platforms where non-devs configure forms
✅ API-first architectures needing runtime flexibility

The Bottom Line

Validation rules change more often than your code.
So why are they in your code?

@teonord/validator's template API separates validation logic from application code.

Result:

  • Faster iteration
  • Less deployment risk
  • Product team autonomy
  • Better compliance
  • Easier A/B testing
  • True multi-tenancy

Try It Today

npm install @teonord/validator
Enter fullscreen mode Exit fullscreen mode

Documentation: @teonord/qrdrobe

Stop deploying validation changes.
Start configuring them.

One More Thing

This isn't just about validation.
It's about rethinking what belongs in code vs configuration.
Validation rules are business logic.
Business logic changes frequently.
Frequently changing things shouldn't require deployments.
@teonord/validator gets this right.

What will you build when validation is just JSON?

Written by a developer who's tired of deploying validation changes.

Want to see this in action? Drop a comment and I'll show you a real production setup.

Give feedback to improve this answer.

Top comments (0)