Skip to content

Validation ​

JinFrame supports response validation through the BaseValidator class. Validators run after the HTTP response is received and populate valid and $validated on the response object. Pass and fail responses are validated independently.

Concepts ​

Pass vs Fail Validators ​

ValidatorWhen it runsCan throw JinValidationError?
validators.passWhen validateStatus returns trueYes — if type: 'exception'
validators.failWhen validateStatus returns falseNo — only sets valid/$validated

Fail validators never throw JinValidationError because doing so would overwrite the original HTTP error.

Validation Result Fields ​

Both JinPassResp and JinFailResp always carry these fields:

ts
interface JinPassResp<T> {
  ok: true;
  data: T;
  valid: boolean;           // false until validator runs and passes
  $validated: ValidationResult;
}

When no validator is configured, valid defaults to true.

Validation Types ​

  • 'exception' — throws JinValidationError on pass-path validation failure
  • 'value' — sets valid: false and populates $validated.error, does not throw

BaseValidator ​

Extend BaseValidator to implement your validation logic. Override getData() to extract the slice of the response you want to validate, and validator() to run the actual check.

ts
import { BaseValidator } from 'jin-frame';
import type { ValidationResult } from 'jin-frame';
import type { JinPassResp } from 'jin-frame';

class UserPassValidator extends BaseValidator<JinPassResp<User>, User, ZodIssue> {
  constructor() {
    super({ type: 'exception' });
  }

  // Extract the data to validate from the full response
  override getData(reply: JinPassResp<User>): User {
    return reply.data;
  }

  override validator(data: User): ValidationResult<ZodIssue> {
    const result = userSchema.safeParse(data);
    if (result.success) return { valid: true };
    return { valid: false, error: result.error.issues };
  }
}

Generic Parameters ​

ParameterDescription
TOriginFull response type passed to validate() (e.g. JinPassResp<User>)
TDataSlice extracted by getData() for actual validation
TErrorType of each item in error[] on failure

Registering Validators ​

Pass validators through the validators option on the method decorator:

ts
@Get({
  host: 'https://api.example.com',
  path: '/users/{id}',
  validators: {
    pass: new UserPassValidator(),
    fail: new UserFailValidator(), // optional
  },
})
class GetUserFrame extends JinFrame<User, ErrorBody> {
  @Param()
  declare public readonly id: string;
}

Example: Zod Validation ​

ts
import { z } from 'zod';
import { BaseValidator, Get, JinFrame } from 'jin-frame';
import type { JinPassResp, ValidationResult } from 'jin-frame';

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof userSchema>;

class UserValidator extends BaseValidator<JinPassResp<User>, User, z.ZodIssue> {
  constructor() {
    super({ type: 'exception' });
  }

  override getData(reply: JinPassResp<User>): User {
    return reply.data;
  }

  override validator(data: User): ValidationResult<z.ZodIssue> {
    const result = userSchema.safeParse(data);
    if (result.success) return { valid: true };
    return { valid: false, error: result.error.issues };
  }
}

@Get({
  host: 'https://api.example.com',
  path: '/users/{id}',
  validators: { pass: new UserValidator() },
})
class GetUserFrame extends JinFrame<User> {
  @Param()
  declare public readonly id: string;
}

try {
  const frame = GetUserFrame.of({ id: '123' });
  const reply = await frame._execute();
  console.log(reply.data); // User — validated
} catch (err) {
  if (err instanceof JinValidationError) {
    console.error(err.validated); // ZodIssue[]
  }
}

Checking Validation Results Without Throwing ​

Use type: 'value' to receive validation results in the response instead of throwing:

ts
class SoftValidator extends BaseValidator<JinPassResp<User>, User, string> {
  constructor() {
    super({ type: 'value' }); // does not throw
  }

  override getData(reply: JinPassResp<User>) { return reply.data; }

  override validator(data: User): ValidationResult<string> {
    if (!data.id) return { valid: false, error: ['missing id'] };
    return { valid: true };
  }
}

const reply = await frame._execute();
if (!reply.valid) {
  console.warn(reply.$validated.error);
}