Валидация загружаемых JSON-файлов

Файлы Lottie представляют собой JSON-описание анимации, экспортированной из After Effects через Bodymovin. Несмотря на формальную структуру, спецификация допускает вариативность, а некоторые экспортеры создают несовместимые или частично некорректные файлы.

Базовая структура Lottie-документа включает ключевые секции:

  • v — версия формата
  • fr — frame rate
  • ip, op — начало и конец анимации
  • layers — слои композиции
  • assets — внешние ресурсы (изображения, precomps)
  • w, h — размеры сцены

Каждый из этих узлов является точкой обязательной проверки перед передачей данных в Lottie Web, поскольку ошибка в любом из них приводит к некорректному рендерингу или полной остановке анимации.


Базовая проверка входного JSON

Первый уровень валидации связан с синтаксисом и типами данных.

function isObject(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value);
}

function validateBasicStructure(data) {
  if (!isObject(data)) return false;

  const requiredFields = ["v", "fr", "ip", "op", "layers"];

  for (const field of requiredFields) {
    if (!(field in data)) return false;
  }

  return true;
}

На этом этапе проверяется только минимальная целостность документа. Однако Lottie допускает вложенные структуры, поэтому этого уровня недостаточно для production-использования.


Проверка типов и диапазонов значений

Следующий слой валидации связан с числовыми ограничениями и типами данных.

Критические поля:

  • fr (frame rate) — положительное число
  • ip, op — целые числа, где op > ip
  • w, h — положительные размеры
  • layers[] — массив объектов
function validateRanges(data) {
  if (typeof data.fr !== "number" || data.fr <= 0) return false;
  if (typeof data.ip !== "number") return false;
  if (typeof data.op !== "number") return false;
  if (data.op <= data.ip) return false;

  if (data.w && data.w <= 0) return false;
  if (data.h && data.h <= 0) return false;

  return Array.isArray(data.layers);
}

Ошибки в этих значениях приводят к невозможности корректного расчета временной шкалы.


Валидация слоёв композиции

Слои являются ядром Lottie-анимации. Каждый слой имеет обязательные поля:

  • ty — тип слоя
  • ind — индекс слоя
  • ks — трансформации
  • shapes или refId (в зависимости от типа)

Типы слоёв включают:

  • 0 — precomp
  • 1 — solid
  • 2 — image
  • 4 — shape
  • 5 — text
function validateLayer(layer) {
  if (typeof layer.ty !== "number") return false;
  if (typeof layer.ind !== "number") return false;
  if (!isObject(layer.ks)) return false;

  const validTypes = [0, 1, 2, 4, 5];
  if (!validTypes.includes(layer.ty)) return false;

  return true;
}

function validateLayers(layers) {
  return layers.every(validateLayer);
}

Особое внимание требуется слоям типа shape (ty: 4), так как они содержат вложенные структуры shapes[], где часто возникают ошибки экспорта.


Глубокая проверка shapes-графов

Shape-структуры представляют собой дерево операторов:

  • gr — группы
  • el — эллипсы
  • rc — прямоугольники
  • sh — пути
  • fl, st — заливки и обводки

Ошибки в этой структуре часто связаны с отсутствием обязательных параметров анимации.

function validateShape(shape) {
  if (!shape || typeof shape.ty !== "string") return false;

  const allowed = ["gr", "el", "rc", "sh", "fl", "st"];
  if (!allowed.includes(shape.ty)) return false;

  return true;
}

function validateShapes(shapes) {
  if (!Array.isArray(shapes)) return false;
  return shapes.every(validateShape);
}

Проверка assets и внешних ресурсов

Раздел assets содержит изображения и precomposition-данные. Ошибки здесь приводят к отсутствию текстур или падению рендера.

Ключевые поля:

  • id
  • w, h
  • u (base path)
  • p (file name)
  • e (embedded flag)
function validateAsset(asset) {
  if (!asset.id) return false;

  if (asset.p && typeof asset.p !== "string") return false;
  if (asset.u && typeof asset.u !== "string") return false;

  return true;
}

function validateAssets(assets) {
  if (!Array.isArray(assets)) return false;
  return assets.every(validateAsset);
}

Схемная валидация через JSON Schema

Для промышленной проверки используется формальная схема. Наиболее распространённый инструмент — AJV.

import Ajv from "ajv";

const ajv = new Ajv({ strict: false });

const lottieSchema = {
  type: "object",
  required: ["v", "fr", "ip", "op", "layers"],
  properties: {
    v: { type: "string" },
    fr: { type: "number", exclusiveMinimum: 0 },
    ip: { type: "number" },
    op: { type: "number" },
    w: { type: "number" },
    h: { type: "number" },
    layers: {
      type: "array",
      items: { type: "object" }
    },
    assets: {
      type: "array",
      items: { type: "object" }
    }
  }
};

const validate = ajv.compile(lottieSchema);

function validateWithSchema(json) {
  return validate(json);
}

Схемная проверка обеспечивает структурную корректность, но не гарантирует семантическую валидность анимации.


Защита от некорректных и вредоносных JSON

Lottie-файлы часто загружаются из внешних источников, что создаёт риск:

  • prototype pollution через ключи __proto__, constructor
  • чрезмерно большие массивы shapes и keyframes
  • бесконечно вложенные структуры
  • подменённые ссылки в assets
function sanitizeKeys(obj) {
  if (!isObject(obj)) return obj;

  const forbidden = ["__proto__", "constructor", "prototype"];

  for (const key of Object.keys(obj)) {
    if (forbidden.includes(key)) {
      delete obj[key];
      continue;
    }

    obj[key] = sanitizeKeys(obj[key]);
  }

  return obj;
}

Ограничение сложности анимации

Для предотвращения деградации производительности вводятся ограничения:

  • максимум слоёв
  • максимум ключевых кадров
  • лимит вложенности shapes
const LIMITS = {
  layers: 200,
  shapesPerLayer: 300,
  keyframesPerProperty: 500
};

function validateComplexity(data) {
  if (data.layers.length > LIMITS.layers) return false;

  for (const layer of data.layers) {
    if (layer.shapes && layer.shapes.length > LIMITS.shapesPerLayer) {
      return false;
    }
  }

  return true;
}

Валидация keyframes и анимационных кривых

Keyframes определяют поведение параметров во времени. Ошибки в них приводят к скачкам или разрыву анимации.

Проверяем:

  • наличие t (time)
  • наличие s (start value)
  • корректность e (end value)
  • отсутствие NaN
function validateKeyframe(kf) {
  if (typeof kf.t !== "number") return false;
  if (!("s" in kf)) return false;

  if (kf.t < 0) return false;

  return true;
}

Пайплайн многоуровневой валидации

Полноценная проверка Lottie JSON строится как последовательность этапов:

  1. Синтаксический JSON.parse
  2. Базовая структура
  3. Проверка диапазонов
  4. Проверка слоёв
  5. Проверка shapes
  6. Проверка assets
  7. Схемная валидация
  8. Санитизация
  9. Проверка сложности
function validateLottie(json) {
  if (!validateBasicStructure(json)) return false;
  if (!validateRanges(json)) return false;
  if (!validateLayers(json.layers)) return false;
  if (!validateAssets(json.assets || [])) return false;
  if (!validateComplexity(json)) return false;

  sanitizeKeys(json);

  return true;
}

Валидация при загрузке в Lottie Web

Перед передачей данных в рендерер используется защитная прослойка.

import lottie from "lottie-web";

async function loadAnimation(url, container) {
  const res = await fetch(url);
  const json = await res.json();

  if (!validateLottie(json)) {
    throw new Error("Invalid Lottie file");
  }

  return lottie.loadAnimation({
    container,
    renderer: "svg",
    loop: true,
    autoplay: true,
    animationData: json
  });
}

Потоковая проверка и Web Worker

При работе с большими файлами валидация выносится в отдельный поток, чтобы не блокировать UI.

// worker.js
self.onmess age = function (e) {
  const json = e.data;

  const result = validateLottie(json);

  self.postMessage({ result });
};

Основной поток получает только результат проверки и принимает решение о загрузке.


Логическая согласованность данных

Помимо структурных ошибок встречаются логические:

  • op меньше максимального keyframe time
  • отсутствие referenced assets
  • слои с одинаковым ind
  • пустые композиции
function validateLogic(data) {
  const indices = new Set();

  for (const layer of data.layers) {
    if (indices.has(layer.ind)) return false;
    indices.add(layer.ind);
  }

  return true;
}