---
title: "Валидация схемы и уникальные индексы"
description: "Валидация схемы mongo в Go: $jsonSchema в CreateCollection и collMod, уровни strict и moderate, warn и error, код 121, unique index mongodb против дублей."
course: "MongoDB в Go: официальный драйвер mongo-driver v2"
url: "https://golangmaster.ru/courses/mongo-driver/lessons/22-indeksy-uniq-schemy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Валидация схемы и уникальные индексы

*Курс «MongoDB в Go: официальный драйвер mongo-driver v2», урок 22 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/mongo-driver/lessons/22-indeksy-uniq-schemy/*

Валидация схемы mongo — это validator на коллекции: документ $jsonSchema, который сервер проверяет на каждой вставке и обновлении, независимо от того, кто пишет. Заведём контракт через CreateCollection, поменяем на живой коллекции через collMod и RunCommand, разберём уровни strict и moderate, действия error и warn, ошибку 121 с деталями — и добавим unique index mongodb против дублей, потому что валидатор форму документа проверяет, а не совпадения.

Сценарий: сервис с проверкой полей в Go работает безупречно, а потом в коллекции появляются документы без email. Это ночной импорт из CSV, коллега с mongosh, скрипт на другом языке — любой клиент, который не обязан знать про проверки вашего кода. Валидация на стороне приложения обязательна, но она живёт в одном клиенте и не видит остальных писателей; валидация схемы mongo переносит последний рубеж на сервер: контракт живёт в коллекции, и обойти его нечем — любой insert и update проходит одну и ту же проверку. Это не замена проверок в коде, а второй рубеж: код отвечает за пользовательские сообщения и быстрые ответы, сервер — за целостность данных при любом источнике записи. Второй рубеж уже стоит: unique index mongodb из семнадцатого урока охраняет совпадения. Здесь добавим форму: обязательные поля, типы BSON, диапазоны чисел и регулярные выражения на строки — через документ $jsonSchema, который заводится при создании коллекции и меняется командой collMod. Нарушителя сервер встречает ошибкой 121 с подробностями, а Go-код ловит её теми же средствами, что и duplicate key в двадцатом уроке.

### $jsonSchema: контракт на языке сервера

Валидатор — обычный BSON-документ с оператором $jsonSchema внутри, а внутри него — подмножество стандарта JSON Schema с BSON-привкусом. Ключ bsonType описывает тип: object для корня документа, string для строк, int и long для целых, double и decimal для дробей, date для дат, objectId для идентификаторов — это BSON-типы, а не строки JSON, и int здесь значит int32 на проводе. Список required называет обязательные поля, словарь properties описывает схему каждого поля: для возраста — bsonType int с минимумом и максимумом, для email — строка с pattern, регулярным выражением, которому строка обязана соответствовать, для статуса — enum с набором допустимых значений. Важное свойство: валидатор — это предикат запроса, наравне с $gt и $exists, поэтому внутри легален любой оператор запроса, а сам $jsonSchema работает не только в описании коллекции, но и в find и в этапе $match конвейера — этим мы воспользуемся для аудита чуть ниже. Вариантов завода два: при создании коллекции — CreateCollection с опциями, для существующей — collMod через RunCommand. Уровни строгости и действия при нарушении настраиваются отдельными опциями, и о них следующий раздел.

### CreateCollection с валидатором

```go
db := client.Database("shop")

validator := bson.M{
        "$jsonSchema": bson.M{
                "bsonType": "object",
                "required": bson.A{"email", "created_at"},
                "properties": bson.M{
                        "email": bson.M{"bsonType": "string"},
                        "age": bson.M{
                                "bsonType": "int",
                                "minimum":  0,
                                "maximum":  130,
                        },
                },
        },
}

err := db.CreateCollection(ctx, "users",
        options.CreateCollection().
                SetValidator(validator).
                SetValidationLevel("strict").
                SetValidationAction("error"))
if err != nil {
        return fmt.Errorf("create users collection: %w", err)
}
```

options.CreateCollection() собирает опции цепочкой Set-методов: SetValidator кладёт контракт, SetValidationLevel и SetValidationAction задают строгость и реакцию — пока строками strict и error. CreateCollection создаёт новую коллекцию с валидатором за один вызов; для существующего имени команда упадёт ошибкой NamespaceExists, поэтому живые коллекции с валидатором меняются только через collMod.

### Уровни и действия: strict, moderate, warn

Параметр validationLevel отвечает за то, кого проверяют. strict — все вставки и все обновления без исключений, значение по умолчанию. moderate — вставки проверяются все, а обновления — только тех документов, которые уже удовлетворяют схеме: документ, родившийся до введения валидатора и не прошедший бы проверку, можно обновлять по полям, не входящим в конфликт с новым контрактом. Это спасает коллекции с легаси-данными: strict на живой коллекции с нарушителями способен заблокировать рядовые обновления, потому что сервер перепроверяет документ целиком, а не только изменённые поля. off выключает проверку совсем — опция для временного отключения без удаления валидатора. Параметр validationAction отвечает за реакцию: error отклоняет нарушителя ошибкой — это дефолт и правильный выбор для новых коллекций; warn пишет предупреждение в журнал сервера, но документ сохраняет. Режим warn полезен на одном этапе: перед включением строгого контракта погоняйте день-другой в warn и посчитайте предупреждения — это честная статистика того, сколько документов strict отрезал бы. Полагаться на warn постоянно не стоит: без мониторинга журнала он превращается в декорацию, и данные тихо плывут дальше. Оба параметра — обычные строки в SetValidationLevel и SetValidationAction, и при смене через collMod они передаются ключами validationLevel и validationAction.

> **Живое место: collMod не перевалидирует старые документы.** Миграция применила строгий валидатор через collMod — команда прошла без ошибок, и все решили, что коллекция теперь чистая. Через месяц обновление старого документа без email падает кодом 121 прямо в пользовательском флоу: strict перепроверяет документ целиком при каждом обновлении, а легаси-нарушители спокойно лежали в базе с момента применения контракта — collMod существующие документы не сканирует и не перевалидирует. Если в коллекции есть документы старше валидатора, выбирайте moderate или планируйте переписывание нарушителей; и гоняйте смену контракта сначала на staging-копии.

### collMod: валидатор на живой коллекции

CreateCollection создаёт коллекцию с валидатором, но только новую: повторный вызов на существующем имени упадёт с ошибкой NamespaceExists, а «перезаписать» контракт он не умеет. Живую коллекцию меняют командой collMod через db.RunCommand: документ команды с ключами collMod, validator и, при желании, validationLevel и validationAction. Здесь стоит нюанс драйвера v2: RunCommand принимает только упорядоченный документ — bson.D, и карта bson.M для команды не годится, что прямо сказано в документации метода. Возвращается *SingleResult: метод Err отдаёт статус команды, Decode при желании разберёт ответ сервера. Текущий контракт читается обратно: ListCollectionSpecifications возвращает спецификации с полем Options типа bson.Raw, из которого Lookup по ключу validator достаёт действующий документ — удобно проверять тестом, что контракт на месте после миграции. Тот же collMod в mongosh — db.runCommand({collMod: ...}) — ведёт себя одинаково: контракты не завязаны на язык клиента, поэтому править их можно любым инструментом, а поведение сверять между mongosh и Go.

### collMod через RunCommand и ловля кода 121

```go
res := db.RunCommand(ctx, bson.D{
        {Key: "collMod", Value: "users"},
        {Key: "validator", Value: validator},
        {Key: "validationLevel", Value: "strict"},
})
if err := res.Err(); err != nil {
        return fmt.Errorf("collMod users: %w", err)
}

_, err := db.Collection("users").InsertOne(ctx, bson.M{
        "name": "документ без email",
})
var se mongo.ServerError
if errors.As(err, &se) && se.HasErrorCode(121) {
        log.Print("validator rejected the document: 121")
}
```

RunCommand принимает bson.D с командой collMod: ключи validator и validationLevel передают новый контракт живой коллекции. SingleResult.Err проверяет статус команды. Вставка-нарушитель получает WriteError кода 121 — DocumentValidationFailure; ServerError достаётся через errors.As из любой обёртки, а HasErrorCode сравнивает код без вскрытия структур.

### Код 121: DocumentValidationFailure и Details

На нарушение валидатора сервер отвечает кодом 121 — у него есть имя DocumentValidationFailure, но в Go код сравнивается числом. У одиночных вставок и обновлений это WriteException со срезом WriteErrors, у InsertMany и BulkWrite — BulkWriteException; правила те же, что выучены в двадцатом уроке про duplicate key: пачка unordered продолжает вставку и собирает все нарушения, ordered останавливается на первой. Проверка кода — метод HasErrorCode(121) интерфейса mongo.ServerError, который достаётся errors.As через любую цепочку обёрток; альтернатива — руками вскрыть WriteErrors и сравнить поле Code, но HasErrorCode короче и покрывает оба типа исключений. Самое ценное лежит в поле Details у WriteError: сервер кладёт туда errInfo — документ с идентификатором нарушителя и деревом несработавших правил схемы, по которому видно и поле, и причину. Драйвер errInfo не парсит — читайте bson.Raw через Lookup, когда нужно отдать пользователю внятную причину, а не общий 400. Отдельно держите в голове warn-режим: там ошибки не будет вовсе, только строки в журнале сервера, и нарушения ищутся не в коде, а в логах. В общей карте ошибок 121 — сосед кода 11000: дубликат говорит «такой ключ уже занят», валидация — «документ не той формы», и оба превращаются в доменные ошибки на границе репозитория.

### Параметры валидации одним взглядом

| Параметр | Что делает | Когда выбирать |
| --- | --- | --- |
| SetValidationLevel("strict") | проверяет все вставки и обновления | дефолт для новых коллекций |
| SetValidationLevel("moderate") | не блокирует обновления уже-невалидных документов | коллекции с легаси-данными |
| SetValidationAction("error") | отклоняет нарушителя WriteError 121 | дефолт; контракт обязателен |
| SetValidationAction("warn") | предупреждение в журнал, документ сохраняется | переходный период перед strict |
| RunCommand с collMod | меняет валидатор живой коллекции | CreateCollection — только для новой |

> **Живое место: валидатор вместо уникального индекса.** Команда закрыла требование «email уникален» валидатором схемы: поле обязательное, тип строка — защита вроде бы есть. Ночной импорт создал два аккаунта на один адрес, и оба прошли проверку формы: $jsonSchema видит только один документ и не сравнивает его с соседями. Если в задании слово «уникальный», то это свойство коллекции, а не документа, и несёт его только unique index mongodb через SetUnique(true), а ловится отказ mongo.IsDuplicateKeyError. Валидатор и индекс — разные рубежи: форма и совпадения, и нужны оба сразу.

### Аудит: найти нарушителей до их блокировки

Поскольку $jsonSchema — обычный оператор запроса, им можно искать документы, которые схеме соответствуют. Инверсия — $nor с $jsonSchema внутри — выбирает всё, что контракту не удовлетворяет: db.users.find с таким предикатом вернёт ровно нарушителей. Это дешёвый аудит перед ужесточением: сначала collMod в warn-режиме, затем посчитать и разглядеть нарушителей запросом, затем включить strict, когда счётчик уйдёт в ноль. Тот же приём находит и обратное — документы, которые контракту соответствуют, если предикат применить без инверсии; для быстрой оценки объёма достаточно countDocuments вместо полной выборки. В конвейере агрегации предикат живёт в этапе $match, поэтому отчёт по нарушителям с группировкой по полям собирается тем же инструментом, что и любой другой отчёт. Аудит-запросы выполняйте на вторичном узле с read preference secondary, если коллекция большая: скан по всей коллекции для отчёта не должен мешать записи. И помните про explain из урока про индексы: $jsonSchema в фильтре не опирается на индексы и честно сканирует — для разового аудита это нормально, для панельного запроса — нет.

### Правила валидации схемы одним списком

- Валидатор — предикат сервера: $jsonSchema с bsonType, required, properties, minimum, pattern, enum.
- Новая коллекция — CreateCollection с SetValidator; живая — collMod через db.RunCommand.
- RunCommand в v2 принимает только упорядоченный bson.D: карта bson.M для команды не годится.
- strict и error — дефолт; moderate выбирают для коллекций с легаси-документами, warn — на переходный период.
- collMod существующие документы не перевалидирует: нарушители проявятся 121 на первом обновлении.
- Нарушение — WriteError 121 через HasErrorCode; уникальность валидатором не решается — нужен unique index mongodb.

### Проверка: вставка нарушителя

**Вопрос:** Коллекция создана с validationLevel strict и validationAction error. Что вернёт InsertOne документа без обязательного поля email?

- Документ вставится с полем email: null
- ✅ WriteException с кодом 121
- Сервер молча допишет пустую строку
- Предупреждение в лог, документ сохранится

strict проверяет все вставки, error превращает нарушение в отказ: WriteError кода 121, у одиночной вставки — WriteException. С полем null и пустой строкой вставки не бывает, а сохранение с предупреждением — это режим warn.

### Проверка: смена контракта

**Вопрос:** Коллекция orders существует и наполнена данными. Как надеть на неё валидатор?

- Повторный CreateCollection с SetValidator
- ✅ RunCommand с командой collMod и ключом validator
- Indexes().CreateOne с SetUnique(true)
- Никак: валидатор задаётся только при создании

CreateCollection работает только для новой коллекции — иначе NamespaceExists. Существующую меняют командой collMod через db.RunCommand с bson.D; уровень и действие передаются ключами validationLevel и validationAction.

### Что напечатает пара вставок?

```go
// коллекция: required ["email"], strict, error
_, err1 := coll.InsertOne(ctx, bson.M{"email": "anna@example.com"})
_, err2 := coll.InsertOne(ctx, bson.M{"name": "без почты"})

var se mongo.ServerError
_ = errors.As(err2, &se)
fmt.Println(err1 == nil, se.HasErrorCode(121))
```

**Что выведет программа?**

- ✅ true true
- true false
- false true
- false false

Первый документ удовлетворяет схеме и вставляется — err1 равен nil. Второй не содержит обязательного email: strict отклоняет его с WriteError 121, и HasErrorCode через ServerError видит этот код.

### Соберите создание коллекции с валидатором

Восстановите вызов: создание коллекции, сборщик опций, валидатор и строгий уровень.

```go
err := db.___(ctx, "users",
        options.___().
                Set___(validator).
                SetValidationLevel("___").
                SetValidationAction("error"))
if err != nil {
        return fmt.Errorf("create users: %w", err)
}
```

Подсказка: Коллекцию создаёт db.CreateCollection; опции собирает options.CreateCollection(); валидатор ставит SetValidator; строгий уровень — строка strict.

Правильные ответы в пропусках: `CreateCollection`, `CreateCollection`, `Validator`, `strict`

### Соберите смену валидатора

Расставьте строки метода: документ команды collMod, вызов RunCommand, проверка статуса.

Правильный порядок строк:

1. `func (r *Users) TightenSchema(ctx context.Context) error {`
2. `  cmd := bson.D{`
3. `    {Key: "collMod", Value: "users"},`
4. `    {Key: "validator", Value: r.validator},`
5. `    {Key: "validationLevel", Value: "strict"},`
6. `  }`
7. `  res := r.db.RunCommand(ctx, cmd)`
8. `  return res.Err()`
9. `}`

### Карточки: валидация схемы

- **$jsonSchema** — Валидатор-документ: bsonType, required, properties, minimum, pattern, enum. Обычный оператор запроса: работает в описании коллекции, в find и в этапе $match конвейера.
- **validationLevel** — strict — проверять все вставки и обновления, дефолт; moderate — не блокировать обновления уже невалидных легаси-документов; off — проверка выключена без удаления валидатора.
- **validationAction** — error — отклонять нарушителя WriteError 121, дефолт; warn — предупреждение в журнал сервера и документ сохраняется. warn требует мониторинга логов, иначе бесполезен.
- **collMod** — Команда смены валидатора живой коллекции: db.RunCommand с bson.D. В v2 RunCommand принимает только упорядоченные документы — bson.M не годится. Существующие документы не перевалидирует.
- **Код 121** — DocumentValidationFailure: WriteError у одиночных записей, BulkWriteException у пачек. HasErrorCode(121) через mongo.ServerError и errors.As; детали нарушителя — в поле Details.
- **Уникальность** — Свойство коллекции, а не документа: только unique index mongodb через SetUnique(true) запретит совпадения, и IsDuplicateKeyError переведёт отказ в доменную ошибку. Валидатор проверяет форму одного документа.

Контракт стоит в базе: обязательные поля, типы и диапазоны проверяет сервер, нарушители получают 121, а уникальность держит отдельный рубеж — индекс. База данных теперь защищает сама, и сервис может сосредоточиться на сообщениях для людей. Дальше по курсу: [урок про индексы](/courses/mongo-driver/lessons/17-indeksy/) стоит перечитать перед заводом unique index mongodb — там правило про упорядоченные Keys и цену каждого индекса для записи; [урок про ошибки драйвера](/courses/mongo-driver/lessons/20-oshibki-drivera/) разложит 121 и 11000 по одной карте — HasErrorCode и IsDuplicateKeyError рядом; а в репозитории из двадцать первого урока обе ошибки превращаются в доменные на границе. Тем, кто работает с Postgres, полезно провести параллель: [уникальность 23505 в pgx](/courses/pgx/lessons/17-unikalnost-23505/) — та же идея «ошибка записи как бизнес-сигнал», только код другой. Проверьте всё руками: песочница сайта не исполняет mongo-go-driver, поэтому поднимите MongoDB локально в Docker, создайте коллекцию с валидатором из урока и вставьте документ без обязательного поля — код 121 и детали в Details вы увидите своими глазами.

## Коротко: что запомнить

- Валидатор — документ $jsonSchema на коллекции: bsonType, required, properties, minimum, pattern, enum.
- Новая коллекция — CreateCollection с SetValidator; живая — collMod через db.RunCommand с bson.D.
- validationLevel: strict — все записи, moderate — щадит легаси-нарушителей, off — выключено; дефолт strict.
- validationAction: error отклоняет кодом 121, warn пишет в журнал и сохраняет; дефолт error.
- collMod не перевалидирует старые документы: нарушители проявятся 121 на первом обновлении.
- Код 121 ловится HasErrorCode через mongo.ServerError; серверные подробности — в Details у WriteError.
- Уникальность — свойство коллекции: только unique index mongodb с SetUnique(true) и IsDuplicateKeyError.
- $jsonSchema — предикат запроса: $nor с ним находит нарушителей перед включением strict.

## Домашнее задание

Поднимите MongoDB и создайте коллекцию users с валидатором из урока: обязательные email и created_at, возраст от нуля до ста тридцати. Вставьте валидный документ, затем три нарушителя: без email, с отрицательным возрастом и с email-числом — каждый раз прочитайте Details у WriteError и найдите failingDocumentId. Переведите коллекцию в warn, вставьте нарушителя и найдите предупреждение в журнале сервера. Затем через collMod поставьте moderate, обновите валидный документ и убедитесь, что обновление прошло, — после этого примените strict к коллекции с нарушителем и повторите.

## Вопросы и ответы по уроку

### Нужна ли валидация схемы, если все проверки уже в Go?

Проверки в коде обслуживают только вашего клиента. Ночной импорт, скрипт на другом языке, коллега в mongosh пишут мимо — и сервер примет что угодно. Контракт в коллекции проверяет всех писателей одной проверкой. Код остаётся: он даёт быстрые и понятные сообщения пользователю до обращения к базе.

### Почему bson.M не подходит для RunCommand?

Команда — упорядоченный документ: имя команды обязано идти первым ключом, и сервер читает его первым. У карты порядок ключей не определён, поэтому RunCommand в v2 принимает только упорядоченные типы вроде bson.D — это прямо написано в документации метода. bson.M остаётся для фильтров и валидаторов, где порядок не важен.

### Что выбрать для коллекции с унаследованными данными?

Начните с warn при strict-уровне: неделя-другая даст честную статистику нарушителей. Затем проведите аудит $nor-запросом и перепишите или почистите нарушителей. После этого строгий уровень и действие error заблокируют только реальные ошибки. Если чистить легаси нельзя, оставьте moderate: обновления старых документов не встанут.
