Unicode-строки и эмодзи в строковой валидации Zod
Работа со строками в JavaScript неизбежно упирается в особенности Unicode. Даже простые операции вроде проверки длины или применения регулярных выражений могут давать неожиданные результаты, когда в данных присутствуют эмодзи, комбинируемые символы или знаки из расширенных письменностей. В Zod это особенно важно, поскольку библиотека активно используется для строгой валидации входных данных в API, формах и доменных моделях.
Строка в JavaScript представляет собой последовательность UTF-16 кодовых единиц, а не абстрактных символов. Это означает, что:
Пример:
"?".length // 2, а не 1
Это напрямую влияет на валидацию в Zod, если использовать стандартные методы проверки длины.
Zod предоставляет базовый тип:
import { z } from "zod";
const schema = z.string();
И набор методов:
min(n) и max(n) — проверяют длину
строки;length(n) — фиксированная длина;regex() — проверка по регулярному выражению;refine() и superRefine() — кастомная
логика.Важно: методы min, max, length
работают на уровне UTF-16 кодовых единиц, а не Unicode-графем.
Это ключевая причина проблем при работе с эмодзи.
Рассмотрим:
const schema = z.string().min(1).max(2);
schema.parse("?");
С точки зрения пользователя это один символ, но для JavaScript — две кодовые единицы. Следовательно:
Особенно это критично для:
Unicode определяет понятие extended grapheme cluster — визуально воспринимаемого символа.
Например:
В JavaScript стандартного API для подсчёта графем нет, но есть современный инструмент:
new Intl.Segmenter("en", { granularity: "grapheme" })
Для точного контроля можно переопределить проверку:
import { z } from "zod";
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const graphemeLength = (str) =>
[...segmenter.segment(str)].length;
const schema = z.string().refine((val) => {
return graphemeLength(val) <= 5;
}, {
message: "Слишком длинная строка"
});
Такой подход переводит валидацию с уровня UTF-16 на уровень пользовательского восприятия.
Регулярные выражения в JavaScript также работают на уровне кодовых единиц. Это создаёт проблемы при попытке:
Пример ошибочного подхода:
const schema = z.string().regex(/^\w+$/);
Эмодзи автоматически не попадают в \w, но и обратное
поведение может быть неожиданным при модификации строк.
Для Unicode-ориентированных проверок используется флаг
u:
/^\p{Emoji}+$/u
Однако поддержка \p{Emoji} зависит от движка и может
быть нестабильной в старых окружениях.
Zod позволяет использовать регулярные выражения напрямую:
const emojiOnly = z.string().regex(/^\p{Extended_Pictographic}+$/u);
Это проверяет, что строка состоит только из эмодзи-пиктограмм.
Но важно учитывать:
Extended_Pictographic;Разные формы записи одного и того же символа могут приводить к несоответствиям:
Пример:
const schema = z.string().transform((val) => val.normalize("NFC"));
Это особенно важно для:
Zod часто используется на сервере, но ограничения длины применяются и в UI. Несоответствие возникает, когда:
Решение — единая функция подсчёта, согласованная между клиентом и сервером.
Zod позволяет строить сложные правила:
const schema = z.string().superRefine((val, ctx) => {
const length = [...new Intl.Segmenter("en", { granularity: "grapheme" }).segment(val)].length;
if (length > 10) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: 10,
type: "string",
inclusive: true,
message: "Превышена длина строки"
});
}
});
Это даёт полный контроль над Unicode-семантикой.
В современных приложениях эмодзи перестают быть просто символами:
Поэтому валидация строк с эмодзи часто требует не только проверки формата, но и семантического анализа.
Стандартные методы Zod:
Поэтому при работе с Unicode-данными Zod выступает скорее как каркас, а не как полный инструмент семантической проверки.
На практике применяется разделение ответственности:
transform — нормализация;refine — ограничения;superRefine — сложные правила.Пример композиции:
const schema = z.string()
.transform((v) => v.normalize("NFC"))
.refine((v) => graphemeLength(v) <= 20);
Такой подход обеспечивает предсказуемость при работе с эмодзи и многоязычными строками.
refine и
superRefine.