Skip to content

api-contract-check ​


Size (builder): 3.94 KBSize (checker): 0.8 KB

This library is included in @arfa-tools/generator and builds serializable validation schemas from TypeScript types. It supports TypeScript aliases for resolving imports, as well as in-memory files (not yet saved to disk) for long transformation chains.

The library is designed for JSON-compatible contracts. Unsupported types fall back to any, which accepts any value. These fallbacks can be logged.

For most scenarios, PluginValidators is sufficient: it already provides an efficient way to use the library. If you cannot use the generator (for example, if you have your own generation pipeline or need custom logic), you can use the API directly.

The generated validation schemas can be used to validate API requests and responses, produce documentation, generate types for other languages, convert to validation schemas for third-party libraries, and more.

Supported types ​

  • any, number, object, boolean, string, null, and string, numeric, and boolean literals;
  • type aliases, interfaces, inline objects, and required and optional properties;
  • inheritance from base interfaces without generic arguments: properties are merged;
  • arrays (T[], Array<T>), fixed-length tuples, and Record<string, T>;
  • substitution of generic type alias parameters, such as type Box<T> = { value?: T };
  • indexed access with a specific object key (User['id']) or an array element (UserList[number]);
  • unions and intersections of objects with compatible overlapping properties;
  • object self-references within arrays, including unions and tuples: children: Array<Node | string>;
  • enums with literal initializers and automatic numeric values, and Enum.Member references;
  • named imports, re-exports, export *, and path aliases from tsconfig.json.

This is enough to describe almost anything that can come from an external API.

Example of supported types and the generated validation schema
ts
enum TypeStatus { Draft = 1, Published, Archived = 7 }
enum TypeChannel { Ready = 'READY', Waiting = 'WAITING' }
enum TypeOwner { Owner = "O'Hara" }

type TypeId = number;
type TypePair = object & { left: string; right?: TypeId }
type TypeTuple = [string, TypeId];
type TypeScores = Record<string, TypeId>;
type TypeVariant = { kind: 'named'; title: string } | { kind: 'sized'; size: TypeId };;
type TypeTreeNode = { name: string; children: Array<TypeTreeNode> };

interface TypeBaseDetails { base: string }
interface TypeInheritedDetails extends TypeBaseDetails { own: TypeId }

type TypeDetails = {
  raw: any;
  tags: Array<string>;
  pair: TypePair;
  exact: 'leaf';
  count: number;
  maybe: number | null;
  mixed: TypeVariant | boolean;
  tuple: TypeTuple;
  labels: Array<'x' | 'y'>;
  choice: string | number;
  record: TypeScores;
  inline: { note: string; marker?: 'hot' };
  active?: boolean;
  enumState: TypeStatus;
  inherited: TypeInheritedDetails;
  recursive: TypeTreeNode;
  metaObject: object;
  enumMember: TypeChannel.Ready;
  escapedEnum: TypeOwner.Owner;
  escapedLiteral: "can't";
};

type TypeDetailsAlias = TypeDetails;
type TypeItem = { 
  id: TypeId; 
  mode: TypeChannel; 
  details: TypeDetailsAlias 
};

type TypeCollection = {
  title: string;
  level4: TypeItem;
  variants: Array<TypeVariant>;
  groups: Array<{
    name: string;
    level4Items: Array<{ code: string; detail: { note: string } }>;
  }>;
};

type TypeData = {
  flag: boolean;
  state: TypeStatus;
  level3: TypeCollection;
  nullable: null;
  literalCombo: 'open' | 3;
};

type TypePayload = {
  requestId: string;
  objectHint: object;
  context: { 
    channel: TypeChannel; 
    batch: Array<TypeId>; 
    level2: TypeData 
  };
};

type TypeRequest = TypePayload;
type TypeResponse = TypePayload;
ts
const positiveRequestSchema = {
  requestId: 'string',
  objectHint: 'object',
  context: {
    channel: ['union', ['literal', 'READY'], ['literal', 'WAITING']],
    batch: ['array', 'number'],
    level2: {
      flag: 'boolean',
      state: ['union', ['literal', 1], ['literal', 2], ['literal', 7]],
      literalCombo: ['union', ['literal', 'open'], ['literal', 3]],
      nullable: ['literal', null],
      level3: {
        title: 'string',
        variants: [
          'array',
          ['union', { kind: ['literal', 'named'], title: 'string' }, { kind: ['literal', 'sized'], size: 'number' }],
        ],
        groups: [
          'array',
          {
            name: 'string',
            level4Items: ['array', { code: 'string', detail: { note: 'string' } }],
          },
        ],
        level4: {
          id: 'number',
          mode: ['union', ['literal', 'READY'], ['literal', 'WAITING']],
          details: {
            exact: ['literal', 'leaf'],
            escapedLiteral: ['literal', "can't"],
            count: 'number',
            raw: 'any',
            metaObject: 'object',
            active: ['optional', 'boolean'],
            enumState: ['union', ['literal', 1], ['literal', 2], ['literal', 7]],
            enumMember: ['literal', 'READY'],
            escapedEnum: ['literal', "O'Hara"],
            maybe: ['union', 'number', ['literal', null]],
            tags: ['array', 'string'],
            labels: ['array', ['union', ['literal', 'x'], ['literal', 'y']]],
            choice: ['union', 'string', 'number'],
            mixed: [
              'union',
              { kind: ['literal', 'named'], title: 'string' },
              { kind: ['literal', 'sized'], size: 'number' },
              'boolean',
            ],
            pair: { left: 'string', right: ['optional', 'number'] },
            record: ['record', 'number'],
            tuple: ['tuple', 'string', 'number'],
            inherited: { base: 'string', own: 'number' },
            inline: { note: 'string', marker: ['optional', ['literal', 'hot']] },
            recursive: ['recursiveArray', { name: 'string', children: ['array', ['self']] }],
          },
        },
      },
    },
  },
} satisfies TypeSchema;

const positiveSchema = {
  request: positiveRequestSchema,
  response: positiveRequestSchema, // same as request
} satisfies TypeRootSchema;

Unsupported types (fall back to any) ​

  • Promise<T>, Map<K, V>, and other unknown generic containers, generic interfaces, and extends Base<T>;
  • Record<K, V> where the key is not specified directly as string;
  • indexed access with dynamic or missing keys, and indexed access on tuples;
  • mapped, conditional, and function types, computed property names, and computed enum initializers;
  • optional and rest tuple elements, and incompatible intersections;
  • direct circular references outside arrays, alias cycles, and recursive roots that are not objects;
  • references through default or namespace imports; these imports are tracked only as file dependencies.

You can track fallbacks through getDegraded. If a union contains an any branch, the entire union becomes any.

Example of unsupported types and the generated validation schema
ts
type UnsupportedMapped = { [K in 'a' | 'b']: number };
type UnsupportedPromise = Promise<string>;
type UnsupportedFunction = (value: string) => number;
type UnsupportedGenericBox = Set<boolean>;
type UnsupportedConditional = string extends number ? 1 : 2;
type UnsupportedVariadicTuple = [string, ...number[]];

interface UnsupportedMethodObject {
  run(input: string): number;
}

interface UnsupportedIndexObject {
  [key: string]: number;
}

interface UnsupportedPayload {
  fn: UnsupportedFunction;
  mapped: UnsupportedMapped;
  promise: UnsupportedPromise;
  indexed: UnsupportedIndexObject;
  voidValue: void;
  genericBox: UnsupportedGenericBox;
  neverValue: never;
  conditional: UnsupportedConditional;
  symbolValue: symbol;
  unknownValue: unknown;
  variadicTuple: UnsupportedVariadicTuple;
  undefinedValue: undefined;
  methods: UnsupportedMethodObject;
}

type TypeRequest = UnsupportedPayload;
type TypeResponse = UnsupportedPromise;
ts
const fallbackSchema = {
  request: {
    fn: 'any',
    mapped: 'any',
    promise: 'any',
    indexed: 'any',
    voidValue: 'any',
    genericBox: 'any',
    neverValue: 'any',
    conditional: 'any',
    symbolValue: 'any',
    unknownValue: 'any',
    variadicTuple: 'any',
    undefinedValue: 'any',
    methods: { run: 'any' },
  },
  response: 'any',
} satisfies TypeRootSchema;

MIT License