Zod умер. Да здравствует ajv-ts

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

TLRD: zod не подходил в проекте и решили сделать свой builder с помощью ajv в zod-like API. Поскольку гугление не показало никаких вменяемых результатов - было решено сделать свои костыли решения.

больше не с zod.
больше не с zod.

Что такое zod?

Zod — это библиотека проверки на уровне схемы с поддержкой типов Typescript.

Сама библиотека очень популярна и используется много где. Самые попуярные и какие я лично использовал это trpc, zodios. Лично мне очень симпатизирует подход zod к описанию структур, поэтому за основу библиотеки я также нагло скопировал позаимствовал некоторые решения.

Краткий экскурс в zod. Давайте представим, что мы хотим создать объект «User» с полями электронной почты и пароля. Оба они - обязательны. В zod мы напишем нечто подобное

import z from "zod";

const UserSchema = z.object({
  username: z.string(),
  password: z.string(),
});

type User = z.infer<typeof UserSchema>; // {username: string, password: string}

const admin = UserSchema.parse({ 
  username: "admin", 
  password: "admin", 
}); // OK

const guestWrong = UserSchema.parse({ 
  username: "admin", 
  password: 123, 
}); // throws Error. Password is not a string

Zod будет обрабатывать входящие аргументы функции "parse" и выдавать ошибку, если аргумент не соответствует схеме.

Почему бы не выбрать zod?


В текущем проекте мы уже используем валидатор схемы ajv. Поскольку у zod есть собственная валидация, которая не поддерживает JSON-Schema и openAPI без плагинов то надо потратить уйму времени для того, чтобы подружить плагины и не факт что это сработает. К тому же часть схем у нас уже была написана, хранилась в обычных js фаилах и валидировалась с помощью ajv. Затягивать еще один валидатор было бы слишком.

Это и есть отправная точка моего приключения под названием ajv-ts.

Попытка 1. Набросаем тип Builder

Создадим класс SchemaBuilder

У него следующая сигнатура - подглядел как это делает zod.

abstract class SchemaBuilder<Input, Schema extends AnySchema, Output = Input> {
  constructor(readonly schema: Schema) {
    // schema is a JSON-schema notation
  }
  safeParse(input: unknown): SafeParseResult {
    // logic here
  }
  parse(input: unknown): Output {
    const { success, data, error } = this.safeParse(input);
    if (success) {
      return data;
    }
    throw error;
  }
  // rest methods
}

Вы можете спросить у меня:

  • Почему класс является абстрактным? Потому что нам не нужно разрешать создание экземпляра SchemaBuilder, тк это некая "общая" схема, те не конкретная(например type: number - это уже конкретная схема)

  • Почему вам нужно определить Output? Функции трансформеры! - мой ответ. Трансформеры — функции, преобразующие входные или выходные результаты. Он работает как до, так и после метода safeParse. И самое главное - позволяет сохранить цепочку входного и выходного generic типа.

Позвольте мне показать вам пример:

import s from 'ajv-ts';

const MySchema = s.string().preprocess((x) => {
  //If we got the date - transform it into "ISO" format
  if (x instanceof Date) {
    return x.toISOString();
  } if (typeof x === 'number'){
    return String(x)
  }
  return x;
}, s.string()); // input: unknown -> string, output: string

const a = MySchema.parse(new Date()); // returns "2023-09-27T12:25:05.870Z"
const b = MySchema.parse(123); // returns "123"

И то же самое для postprocess. Идея проста. Метод parse анализирует входной тип и возвращает выходной.

Пример с NumberSchemaBuilder

class NumberSchemaBuilder extends SchemaBuilder<number, NumberSchema> {
  constructor() {
    super({ type: "number" });
  }
  format(type: "int32" | "double") {
    this.schema.format = type;
    return this
  }
}

Общий входной параметр определяет тип номера.

Идея заключается в манипуляциях с JSON-схемой, потому что многие валидаторы понимают JSON-схему — это стандарт индустрии(привет, zod)

У zod есть собственный парсер, и он не учитывает JSON-Schema. Привет bigint, function, Map, Set, symbol

Бонус: определим числовую функцию:

export function number() {
  return new NumberSchema();
}

Попытка 2. Infer и вывод типов

Как zod понимает, какого типа ваша схема. Я имею в виду, как работает z.infer?

Как вы, возможно, обнаружили, что Output — это именно тот тип, который нам нужен. Это означает, что вам нужно только вызвать Output который является generic, но как это возможно вызвать? NumberSchema не имеет такого параметра, как Output, есть только SchemaBuilder.

Есть несколько путей, но самый простой, это создание пустого свойства с нужным нам типом. Давайте снова переключися на над абстрактный класс и вы все поймете

abstract class SchemaBuilder<Input, Schema extends AnySchema, Output = Input> {
  _input: Input;
  _output: Output;
  // ...other methods
}

_input и _output свойства всегда в JS Runtime будут равны undefined, эти свойста нужны только для Infer типа. Давайте его определим

export type Infer<S extends SchemaBuilder<any, any, any>> = S["_output"];

Теперь мы можем проверить, что это работает:

const MyNumber = s.number()
type Infered = Infer<typeof MyNumber>; // number
const b: number = 3
type B = Infer<typeof b> // never

Все вместе

abstract class SchemaBuilder<Input, Schema extends AnySchema, Output = Input> {
  // type helpers only
  _input: Input;
  _output: Output;

  // JSON-schema
  schema: Schema
  // ajv Instance, used for validation
  ajv: Ajv
  
  safeParse(input: unknown): SafeParseResult {
    try {
      const isValid = this.ajv.validate(input, this.schema)
      return {
        success: true,
        data: input,
      }
    } catch (e){
      return {
        error: this.ajv.errors[0],
        success: false
      }
    }
  }
  parse(input: unknown); // implementation
}
class NumberSchemaBuilder extends SchemaBuilder<number, NumberSchema> {
  constructor() {
    super({ type: "number" });
  }
  format(type: "int32" | "double") {
    this.schema.format = type;
  }
}
export function number() {
  return new NumberSchema();
}

Мои Заключения

Главное достигнуто — мы определили строитель JSON-схемы который по своему апи довольно близок к zod api! И это потрясающе!

Библиотека имеет подобный API, что и Zod (но к сожалению не все можно сконвертировать 1-1). К тому же, я позволил себе некоторые вольности, а теперь пытаюсь сделать api еще более похожим на zod.

Если вы спросите меня: "Стоило ли оно того?" Одназначно да! - отвечу я. Во-первых, я сильно прокачался в typescript, generics и infer types. Во-вторых, сравните сами: какой подход более наглядный 12 строк в JSON-schema или 4 в ajv-ts?

const Schema1 = {
  type: 'object',
  properties: {
    "transferId": {
      "type": "string",
      "nullable": true
    },
    "deduplicationId": {
      "type": "string",
      "nullable": true
    }
  },
}

const Schema2 = s.object({
  transferId: s.string().nullable(),
  deduplicationId: s.string().nullable(),
})
Schema2.schema // тоже самое что и Schema1.

В-третьих, теперь любая команда проекта может легко и просто определять схемы, а также JSON-схемы написанные нашей командой поставляются другим командам, кол-во опечаток также уменьшилось, а кол-во радостных коллег увеличилось

Источник: https://habr.com/ru/articles/789384/


Интересные статьи

Интересные статьи

Привет! В одном из прошлых постов мы рассказывали вам, что в МКБ пришел Главный архитектор (ГА), Клецких Дмитрий. Проанализировав и оценив состояние дел, новый руководитель занялся изменениями, внедре...
Twitter, социальная сеть и служба микроблогов, оказала огромное влияние на политику и культуру XXI века. Хэштеги и ретвиты — лишь некоторые термины из Twitter. За 17 лет соцсеть прошла через многое,...
4 года назад наша команда Nixys рассказывала, почему мы решили сделать собственный инструмент для резервного копирования и почему другие инструменты нам не подошли. Сегодня хочу ра...
Рынок труда заполняется представителями поколения Z — людьми, родившимися в период с конца 90-х до начала 2000-х. У поколения есть отличительные черты, с которыми многие компании пока не готовы считат...
Прошло несколько месяцев с тех пор, как я здесь рассказал о своем проекте Qt-based библиотеки для сериализации данных из объектного вида в JSON/XML и обратно. И как бы я не гордилс...