Skip to main content

Overview

Who To Bother uses Valibot for runtime validation of all company JSON files. This ensures data integrity and provides helpful error messages when validation fails.

Why Valibot?

Valibot is a modern schema validation library that provides:
  • Type Safety: TypeScript types are inferred directly from the schema
  • Runtime Validation: Validates data at build time and runtime
  • Single Source of Truth: One schema definition for both validation and types
  • JSON Schema Generation: Auto-generate JSON Schema for IDE support
  • Detailed Error Messages: Clear, actionable error messages
The build will fail if any JSON file fails validation, preventing deployment of invalid data.

Schema Definition

The Valibot schema is defined in src/data/companies/schema.ts:
src/data/companies/schema.ts
import {
  array,
  email,
  type InferOutput,
  minLength,
  object,
  optional,
  pipe,
  regex,
  string,
  url,
} from "valibot";

// Contact schema - represents a single contact with product/role and handles
export const ContactSchema = object({
  product: pipe(string(), minLength(1, "Product name is required")),
  handles: pipe(
    array(
      pipe(
        string(),
        regex(
          /^@[a-zA-Z0-9_]+$/,
          "Handle must start with @ and contain only letters, numbers, and underscores"
        )
      )
    ),
    minLength(1, "At least one handle is required")
  ),
  email: optional(pipe(string(), email("Must be a valid email address"))),
  discord: optional(pipe(string(), url("Must be a valid URL"))),
});

// Category schema - represents a category of contacts
export const CategorySchema = object({
  name: pipe(string(), minLength(1, "Category name is required")),
  contacts: pipe(
    array(ContactSchema),
    minLength(1, "At least one contact is required per category")
  ),
});

// Company schema - represents a complete company with all contact information
export const CompanySchema = object({
  $schema: optional(string()),
  id: pipe(
    string(),
    minLength(1, "Company ID is required"),
    regex(
      /^[a-z0-9-]+$/,
      "Company ID must be lowercase with only letters, numbers, and hyphens"
    )
  ),
  name: pipe(string(), minLength(1, "Company name is required")),
  description: pipe(string(), minLength(1, "Company description is required")),
  logoType: pipe(string(), minLength(1, "Logo type is required")),
  logo: optional(string()),
  website: optional(pipe(string(), url("Must be a valid URL"))),
  docs: optional(pipe(string(), url("Must be a valid URL"))),
  github: optional(pipe(string(), url("Must be a valid URL"))),
  discord: optional(pipe(string(), url("Must be a valid URL"))),
  categories: pipe(
    array(CategorySchema),
    minLength(1, "At least one category is required")
  ),
});

// Infer TypeScript types from schemas
export type Contact = InferOutput<typeof ContactSchema>;
export type Category = InferOutput<typeof CategorySchema>;
export type Company = InferOutput<typeof CompanySchema>;

Validation Rules

The schema enforces the following validation rules:

Contact Validation

product
string
required
Must be a non-empty string
pipe(string(), minLength(1, "Product name is required"))
handles
array
required
  • Must be an array with at least one handle
  • Each handle must start with @
  • Can only contain letters, numbers, and underscores
  • No spaces or special characters (except _)
pipe(
  array(
    pipe(
      string(),
      regex(/^@[a-zA-Z0-9_]+$/, "Handle must start with @ and contain only letters, numbers, and underscores")
    )
  ),
  minLength(1, "At least one handle is required")
)
Valid examples: @timneutkens, @shadcn, @user_123Invalid examples: timneutkens (missing @), @user name (space), @user-name (hyphen)
email
string
Optional field that must be a valid email format if provided
optional(pipe(string(), email("Must be a valid email address")))
discord
string
Optional field that must be a valid URL if provided
optional(pipe(string(), url("Must be a valid URL")))

Category Validation

name
string
required
Must be a non-empty string
pipe(string(), minLength(1, "Category name is required"))
contacts
array
required
Must be an array with at least one contact
pipe(
  array(ContactSchema),
  minLength(1, "At least one contact is required per category")
)

Company Validation

id
string
required
  • Must be a non-empty string
  • Can only contain lowercase letters, numbers, and hyphens
  • No uppercase, spaces, or special characters (except hyphen)
pipe(
  string(),
  minLength(1, "Company ID is required"),
  regex(
    /^[a-z0-9-]+$/,
    "Company ID must be lowercase with only letters, numbers, and hyphens"
  )
)
Valid examples: vercel, tanstack, google-ai-studioInvalid examples: Vercel (uppercase), tan_stack (underscore)
name
string
required
Must be a non-empty string (can contain any characters)
pipe(string(), minLength(1, "Company name is required"))
description
string
required
Must be a non-empty string
pipe(string(), minLength(1, "Company description is required"))
logoType
string
required
Must be a non-empty string that matches a key in company-logos.tsx
pipe(string(), minLength(1, "Logo type is required"))
categories
array
required
Must be an array with at least one category
pipe(
  array(CategorySchema),
  minLength(1, "At least one category is required")
)
website, docs, github, discord
string
Optional fields that must be valid URLs if provided
optional(pipe(string(), url("Must be a valid URL")))

Validation Script

The validation script (src/scripts/validate-companies.ts) validates all company JSON files:
src/scripts/validate-companies.ts
#!/usr/bin/env tsx

import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { parse, ValiError } from "valibot";
import { CompanySchema } from "../data/companies/schema.js";

const COMPANIES_DIR = join(__dirname, "../data/companies");
const EXCLUDE_FILES = ["schema.json", "example.json.template"];

/**
 * Validates a single company file
 */
function validateFile(file: string): ValidationResult {
  const filePath = join(COMPANIES_DIR, file);
  const result: ValidationResult = {
    file,
    success: false,
  };

  try {
    const content = readFileSync(filePath, "utf-8");
    const data = JSON.parse(content);
    parse(CompanySchema, data);
    result.success = true;
    console.log(`βœ… ${file}`);
  } catch (error) {
    result.success = false;
    result.errors = formatValidationError(error);
    console.log(`❌ ${file}`);
    if (result.errors) {
      for (const err of result.errors) {
        console.log(err);
      }
    }
  }

  return result;
}

Running Validation

Validate all company files with:
pnpm validate
The script will:
  1. Read all .json files from src/data/companies/
  2. Exclude schema.json and example.json.template
  3. Validate each file against the Valibot schema
  4. Print results with checkmarks (βœ…) for valid files and crosses (❌) for invalid files
  5. Show detailed error messages for any validation failures
  6. Exit with code 1 if any files fail validation (prevents builds)

Example Output

πŸ” Validating company JSON files...

βœ… vercel.json
βœ… tanstack.json
βœ… cloudflare.json
βœ… supabase.json

==================================================

πŸ“Š Validation Summary:
   Total files: 4
   βœ… Passed: 4
   ❌ Failed: 0

βœ… All company JSON files are valid!

JSON Schema Generation

A JSON Schema is automatically generated from the Valibot schema for IDE support:
pnpm generate-schema
This generates src/data/companies/schema.json from the Valibot schema using @valibot/to-json-schema.

IDE Autocomplete

To enable IDE autocomplete, add the $schema property to your company JSON files:
{
  "$schema": "./schema.json",
  "id": "yourcompany",
  ...
}
This provides:
  • Autocomplete: Suggestions as you type
  • Inline validation: Red squiggly lines for errors
  • Hover documentation: Tooltips with field descriptions
  • Error messages: Detailed error messages before you run validation
Most modern IDEs (VS Code, WebStorm, etc.) automatically recognize the $schema property and provide validation and autocomplete.

Build-Time Validation

Validation runs automatically during the build process:
pnpm build
The build script includes validation as a prebuild step:
package.json
{
  "scripts": {
    "build": "pnpm validate && vite build && tsc --noEmit"
  }
}
If validation fails, the build will fail and no files will be deployed.
The build will fail if any JSON file fails validation. This prevents deployment of invalid data.

Type Safety

TypeScript types are inferred directly from the Valibot schema:
import type { Company, Category, Contact } from "./schema";

// These types are automatically inferred from the Valibot schema
// No need to maintain separate type definitions!
This ensures that:
  • TypeScript types always match the runtime validation
  • No duplication between types and validation
  • Single source of truth for data structure

Common Validation Errors

Error: Handle must start with @ and contain only letters, numbers, and underscoresCause: Handle doesn’t start with @ or contains invalid charactersFix: Ensure all handles:
  • Start with @
  • Contain only letters, numbers, and underscores
  • No spaces, hyphens, or other special characters
// ❌ Wrong
"handles": ["timneutkens", "@user name", "@user-name"]

// βœ… Correct
"handles": ["@timneutkens", "@user_name"]
Error: Company ID must be lowercase with only letters, numbers, and hyphensCause: Company ID contains uppercase letters or invalid charactersFix: Use only lowercase letters, numbers, and hyphens:
// ❌ Wrong
"id": "YourCompany", "id": "your_company"

// βœ… Correct
"id": "yourcompany", "id": "your-company"
Error: Company ID is required (or other field name)Cause: A required field is missingFix: Ensure all required fields are present:
  • id, name, description, logoType
  • At least one category with name and contacts
  • Each contact must have product and handles
Error: At least one handle is required or At least one contact is required per categoryCause: Required arrays are emptyFix: Ensure:
  • Each contact has at least one handle
  • Each category has at least one contact
  • The company has at least one category
Error: Must be a valid email addressCause: Email field doesn’t match email formatFix: Use a valid email format:
// ❌ Wrong
"email": "not-an-email"

// βœ… Correct
"email": "[email protected]"
Error: Must be a valid URLCause: URL fields don’t match URL formatFix: Use complete, valid URLs:
// ❌ Wrong
"website": "company.com"

// βœ… Correct
"website": "https://company.com"

Best Practices

Use IDE autocomplete

Add "$schema": "./schema.json" to enable IDE validation and autocomplete

Validate early and often

Run pnpm validate frequently while editing to catch errors early

Read error messages

Valibot provides detailed, actionable error messages - read them carefully

Test locally

Always test with pnpm dev after validation passes

Further Reading

Build docs developers (and LLMs) love