Emoji и Unicode

Unicode-строки и эмодзи в строковой валидации Zod

Работа со строками в JavaScript неизбежно упирается в особенности Unicode. Даже простые операции вроде проверки длины или применения регулярных выражений могут давать неожиданные результаты, когда в данных присутствуют эмодзи, комбинируемые символы или знаки из расширенных письменностей. В Zod это особенно важно, поскольку библиотека активно используется для строгой валидации входных данных в API, формах и доменных моделях.

Строка в JavaScript представляет собой последовательность UTF-16 кодовых единиц, а не абстрактных символов. Это означает, что:

  • базовые символы (A, B, C) занимают одну кодовую единицу;
  • символы вне базовой многоязычной плоскости (BMP), включая большинство эмодзи, кодируются двумя кодовыми единицами (суррогатные пары);
  • один «видимый символ» может состоять из нескольких Unicode code points.

Пример:

"?".length // 2, а не 1

Это напрямую влияет на валидацию в Zod, если использовать стандартные методы проверки длины.

Поведение 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 — две кодовые единицы. Следовательно:

  • логика может быть неожиданной;
  • ограничения длины становятся неточными для UI и UX.

Особенно это критично для:

  • никнеймов;
  • сообщений;
  • тегов;
  • реакций и эмодзи-реакций.

Графемные кластеры и реальная «длина символа»

Unicode определяет понятие extended grapheme cluster — визуально воспринимаемого символа.

Например:

  • ?? (флаг) — комбинация региональных индикаторов;
  • ?‍?‍?‍? — семья, составленная из нескольких эмодзи и joiner-символов;
  • á — буква + комбинируемый акцент.

В JavaScript стандартного API для подсчёта графем нет, но есть современный инструмент:

new Intl.Segmenter("en", { granularity: "grapheme" })

Корректная валидация длины через Zod и Segmenter

Для точного контроля можно переопределить проверку:

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 и Unicode-паттерны

Zod позволяет использовать регулярные выражения напрямую:

const emojiOnly = z.string().regex(/^\p{Extended_Pictographic}+$/u);

Это проверяет, что строка состоит только из эмодзи-пиктограмм.

Но важно учитывать:

  • не все эмодзи входят в Extended_Pictographic;
  • комбинированные последовательности могут не распознаваться как единое целое.

Нормализация Unicode перед валидацией

Разные формы записи одного и того же символа могут приводить к несоответствиям:

  • NFC (Normalization Form C)
  • NFD (Normalization Form D)

Пример:

const schema = z.string().transform((val) => val.normalize("NFC"));

Это особенно важно для:

  • имен пользователей;
  • поисковых запросов;
  • сравнений строк в базе данных.

Проблема визуальной длины и UI-валидации

Zod часто используется на сервере, но ограничения длины применяются и в UI. Несоответствие возникает, когда:

  • клиент считает эмодзи за 1 символ;
  • сервер считает за 2 кодовые единицы;
  • или за несколько grapheme clusters.

Решение — единая функция подсчёта, согласованная между клиентом и сервером.

Кастомная логика через superRefine

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

Стандартные методы Zod:

  • не учитывают grapheme clusters;
  • работают на уровне UTF-16;
  • не различают сложные эмодзи-композиции;
  • не нормализуют строки автоматически.

Поэтому при работе с Unicode-данными Zod выступает скорее как каркас, а не как полный инструмент семантической проверки.

Практический паттерн интеграции Unicode-логики

На практике применяется разделение ответственности:

  • Zod — базовая структура и типы;
  • кастомные функции — Unicode-логика;
  • transform — нормализация;
  • refine — ограничения;
  • superRefine — сложные правила.

Пример композиции:

const schema = z.string()
  .transform((v) => v.normalize("NFC"))
  .refine((v) => graphemeLength(v) <= 20);

Такой подход обеспечивает предсказуемость при работе с эмодзи и многоязычными строками.

Итоговые особенности работы с Unicode в Zod-валидации

  • строка JavaScript ≠ визуальные символы;
  • эмодзи часто занимают несколько кодовых единиц;
  • стандартные методы Zod работают на уровне UTF-16;
  • корректная работа требует grapheme segmentation;
  • регулярные выражения должны учитывать Unicode-флаги;
  • нормализация строки критична перед сравнением и хранением;
  • сложная валидация реализуется через refine и superRefine.