Skip to content

refactor: unify targets validation into Zod schema (single source of truth) #909

@christso

Description

@christso

Problem

Target validation is split across two systems that must stay in sync:

  1. Zod schema (BASE_TARGET_SCHEMA in targets.ts) — parses and types target definitions
  2. Manual validator (targets-validator.ts) — pre-resolution checks (env var syntax, unknown fields, provider validation)

Adding use_target required changes in 3 places: TargetDefinition interface, Zod schema, and the manual validator. This is brittle — each new field risks the two systems diverging.

Proposal

Make the Zod schema the single source of truth. Move validator logic into Zod .refine() and .superRefine():

const TargetSchema = z.object({
  name: z.string().min(1),
  provider: z.string().optional(),
  use_target: z.string().optional(),
  grader_target: z.string().optional(),
  // ...
}).superRefine((data, ctx) => {
  // Provider required unless use_target is set
  if (!data.use_target && !data.provider) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Either 'provider' or 'use_target' is required",
      path: ['provider'],
    });
  }
  // Env var syntax validation
  if (data.provider && /^\$\{\{/.test(data.provider)) {
    // validate env var format
  }
});

Benefits

  • One schema defines structure, types, AND validation rules
  • TargetDefinition type is inferred from schema (z.infer<typeof TargetSchema>)
  • Adding a new field = one change in one place
  • Validation errors include Zod's structured path information

Prior art

  • Vitest: single Zod schema for config validation + typing
  • tRPC: Zod schemas as single source of truth for input validation
  • Astro: Zod content collections schema (validates + types in one pass)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions