---
title: "JSON, TOML и другие форматы конфигов"
description: "viper json toml: какие форматы конфигов go поддерживает viper, порядок SupportedExts при поиске, SetConfigType и ReadConfig. Выбор формата с примерами."
course: "Viper: конфигурация приложений на Go"
url: "https://golangmaster.ru/courses/viper/lessons/03-formaty-configov/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 16
---

# JSON, TOML и другие форматы конфигов

*Курс «Viper: конфигурация приложений на Go», урок 3 из 25. ~16 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/viper/lessons/03-formaty-configov/*

Переписывать код чтения под каждый формат конфигов не станет никто: viper json toml и остальные форматы читает одним API, парсер выбирается по расширению файла, а программа не меняется. Ситуация знакомая: соседний отдел живёт в TOML, генератор деплоя выплёвывает JSON, легаси-сервис тянет ini. Разберём список форматов, порядок поиска файла и чтение конфига из строки.

Второй урок курса закончился на ReadInConfig: [файл config.yaml найден и прочитан](/courses/viper/lessons/02-pervyj-yaml/), значения поехали в программу. Ровно здесь команды обычно затыкаются: за пределами туториалов конфиги живут не только в YAML — инфраструктура соседнего отдела хранит настройки в TOML, генератор деплоя выплёвывает JSON, а легаси-сервис тянет за собой ini. Переписывать код чтения под каждый формат не станет никто, и правильно: viper читает всё одним API, парсер выбирается по расширению файла, а программа не меняется ни на символ. В этом уроке — полный список форматов, поиск файла без расширения, фиксированный порядок перебора расширений и чтение конфига прямо из строки или потока. Плюс одна ловушка, на которой команды теряют недели: что победит, если в папке лежат два конфига разных форматов.

### Форматы конфигов go: что читает viper из коробки

Список длиннее, чем ожидают: yaml и yml, json, toml, hcl — формат HashiCorp, ini, properties с вариантами props и prop, javaproperties, dotenv и env, tfvars из мира Terraform. Отдельно стоит env: переменные окружения приходят не из файла, а из окружения процесса и подключаются через AutomaticEnv — о ней в курсе свой урок. Все файловые форматы устроены одинаково: на ReadInConfig viper определяет формат и вызывает соответствующий парсер, а распарсенное дерево целиком ложится в единый реестр ключей. Дальше источнику без разницы: GetInt и GetString работают с toml-конфигом ровно так же, как с yaml, и семейство Get об этом даже не узнает. Отдельные парсеры подключать не нужно — всё уже внутри библиотеки. Переключение формата — это замена файла и, в отдельных случаях, одной строки настройки, но не правка кода чтения.

### Поиск файла без расширения

```go
viper.SetConfigName("config") // без расширения!
viper.AddConfigPath("./configs")
viper.AddConfigPath(".")      // запасной вариант: текущая папка

err := viper.ReadInConfig()
if err != nil {
        log.Fatal("конфиг не найден:", err)
}
```

SetConfigName задаёт имя без расширения — расширение viper подберёт сам. Пути добавляются в порядке приоритета: сначала ./configs, потом текущая папка. ReadInConfig перебирает каталоги, а внутри каждого — поддерживаемые расширения, и берёт первый найденный файл. Ошибка возвращается только когда ни один путь ничего не дал.

### Порядок SupportedExts: viper выбирает файл сам

Раз расширение подбирается автоматически, напрашивается вопрос: а в каком порядке перебираются варианты? Порядок фиксированный и зашит в переменную viper.SupportedExts: json, toml, yaml, yml, properties, props, prop, hcl, tfvars, dotenv, env, ini. Логика поиска простая: в каждом каталоге из AddConfigPath viper идёт по этому списку слева направо и берёт первый существующий файл. Отсюда два следствия. Первое: в одной папке json всегда сильнее toml, а toml — сильнее yaml, независимо от того, какой файл вы считали главным. Второе: порядок через API не настраивается — ни переставить расширения, ни отдать приоритет yaml нельзя; единственный способ зафиксировать выбор жёстко — задать файл явно через SetConfigFile, который поиск отключает вовсе. Для парсера форматы равноправны, но не для поиска: разница между «поддерживается» и «будет найдено первым» здесь принципиальная, и о ней следующая врезка.

> **config.yaml и config.json в одной папке — классика.** В configs/ лежит config.yaml, туда же падает config.json — сгенерировал инструмент деплоя или коллега положил «на пробу». Viper молча берёт json: он стоит в SupportedExts раньше. Программа работает, но правки в yaml не применяются — и вы неделю проверяете отступы, кавычки и кеш, пока не заметите второй файл. Лечение: один формат на каталог либо SetConfigFile с точным путём.

### Тот же код читает TOML

```go
package main

import (
        "fmt"
        "log"

        "github.com/spf13/viper"
)

func main() {
        viper.SetConfigFile("configs/config.toml")

        if err := viper.ReadInConfig(); err != nil {
                log.Fatal(err)
        }

        fmt.Println(viper.GetString("server.host"), viper.GetInt("server.port"))
}
```

configs/config.toml здесь содержит секцию [server] со строками host = "0.0.0.0" и port = 8080. Формат определяется по расширению toml — никаких SetConfigType не нужно. Замените файл на config.yaml с тем же деревом — код не изменится ни на символ: парсер другой, реестр тот же. Вложенные ключи читаются через точку: server.port — это port внутри секции server.

### SetConfigType и ReadConfig: конфиг не из файла

Файл — не единственный источник. Строка с настройками может прийти из переменной окружения, из stdin, из расшифрованного секрета или из тестовой фикстуры — viper умеет парсить любой io.Reader через ReadConfig. Но у потока нет расширения: откуда парсеру знать формат? Для этого существует SetConfigType — она задаёт формат явно и обязательна перед ReadConfig: без неё вернётся ошибка Unsupported Config Type, viper содержимое не угадывает. Та же настройка спасает для файлов без расширения: SetConfigFile("configs/config") плюс SetConfigType("yaml") читается нормально. И честная оговорка по курсу: чтение файлов и потоков — это файловый ввод-вывод, которого в браузерной песочнице нет; запускайте примеры этого урока локальным Go, где папка configs лежит рядом с main.go.

### JSON из строки

```go
payload := []byte("{"app": {"name": "billing", "port": 8080}}")

viper.SetConfigType("json")
if err := viper.ReadConfig(bytes.NewReader(payload)); err != nil {
        log.Fatal(err)
}

fmt.Println(viper.GetString("app.name"), viper.GetInt("app.port"))
```

Порядок обязателен: сначала SetConfigType("json"), затем ReadConfig с любым io.Reader — bytes.NewReader подходит идеально. Вложенность json превращается в привычные ключи с точками: app.name. Перепишите ту же структуру в yaml и поменяйте тип — вызовы чтения останутся прежними. На этом приёме строятся тесты: фикстура в памяти вместо файла на диске.

### Какой формат конфига выбрать

Критерий не «какой круче», а кто и как правит файл. yaml — выбор по умолчанию для читаемых сервисных конфигов: отступы, комментарии, якоря для повторяющихся кусков; файл редактируют люди руками. json — когда конфиг читает не человек, а машина: генерируется деплоем, отдаётся между сервисами, диффается инструментами; комментариев нет, и для машинного обмена это честно. toml — территория rust-экосистемы и cargo-подобных проектов: если в команде есть рустовщики или репозиторий к ним тяготеет, toml снимает вечный спор о табах и пробелах — там таблицы вместо отступов. ini — для legacy: старые сервисы, настройки Windows-приложений, всё, что уже существует и его жалко переписывать; вложенность в ini бедная, и тащить туда новые проекты я бы не стал. properties и dotenv — для контейнеров и пар вида KEY=value. И спорный совет напоследок: держите один формат на проект, даже когда viper позволяет смешивать — два формата удваивают число мест, где можно промахнуться.

### Ниши форматов

| Формат | Сильная сторона | Типичное место | Нюанс |
| --- | --- | --- | --- |
| yaml | отступы, комментарии, читаемость | сервисные конфиги, правит человек | чувствителен к отступам; json в поиске сильнее |
| json | машинный обмен, парсер в любой среде | сгенерированные конфиги, обмен сервисов | нет комментариев, диффы шумные |
| toml | таблицы вместо отступов, строгий синтаксис | rust-проекты, cargo-стиль | массивы таблиц поначалу путают |
| ini | простота, наследие | legacy-сервисы, настройки Windows | плоская вложенность, всё — строки |
| properties, dotenv | пары KEY=value | env-файлы деплоя, java-наследие | типов и вложенности нет |

ReadInConfig возвращает разные ошибки для разных провалов, и различать их стоит с первого дня. Когда ни один путь не дал файла, прилетает viper.ConfigFileNotFoundError — тип, который проверяют через errors.As. Это штатная ситуация, а не авария: программа может продолжить жизнь на значениях по умолчанию, и как готовить её к конфигу-которого-нет, разобрано в уроке про [SetDefault](/courses/viper/lessons/05-setdefault/). Совсем другое дело — ошибка парсинга: файл нашёлся, но синтаксис битый — незакрытая кавычка, таб вместо пробелов в yaml. Конфиг при этом не загружается целиком: viper не читает «половину файла», и это правильное поведение — запуск с полузагруженными настройками хуже падения на старте. Сообщение называет файл и причину, но не строку — blame по недавнему коммиту быстрее гаданий. Третьей категории, «файл есть, но ключа в нём нет», для ReadInConfig не существует: отсутствующий ключ — не ошибка чтения, а вопрос к тому, кто читает значения, и об этом следующий урок.

### Признаки, что формат выбран плохо

- Диффы конфига в ревью нечитабельны: одна правка порождает десять строк шума — формат не для человека.
- Каждый новый ключ приходится объяснять коллегам вслух: комментариев нет, а это json, который правят руками.
- Три уровня вложенности, и половина багов — забытый отступ: глубокие деревья в ini и properties не живут.
- Одна и та же настройка описана в двух файлах разных форматов, и никто не помнит, какой победит, — см. порядок SupportedExts.
- Конфиг генерируется скриптом, а правится руками, и каждый прогон сносит комментарии: машинный и человеческий форматы смешаны в одном файле.

> **Один формат на каталог.** Мой рабочий принцип: yaml — людям, json — машинам, и не смешивать в одном каталоге. Спорная половина — «никогда не смешивать»: в монорепо машинные json-файлы живут рядом с рукописными yaml, и это нормально, пока пути поиска разведены по разным папкам. Граница простая: если два файла попадают в один AddConfigPath — оставляйте один формат.

### Проверка: какой файл победит

**Вопрос:** В папке ./configs лежат три файла: config.yaml, config.json и config.toml. Задан SetConfigName("config"), SetConfigFile не задан. Какой файл прочитает ReadInConfig?

- ✅ config.json: json стоит в SupportedExts раньше остальных
- config.yaml: yaml — основной формат viper
- config.toml: приоритет отдаётся последнему поддержанному формату
- viper вернёт ошибку: три формата в одной папке конфликтуют

Поиск идёт по фиксированному списку SupportedExts: json, toml, yaml, yml и дальше — в каждом каталоге берётся первый существующий файл. json проверяется раньше yaml и toml, поэтому победит config.json. Никакого конфликта viper не заметит и предупреждать не станет.

### Проверка: ReadConfig без типа

**Вопрос:** Вы вызываете viper.ReadConfig(bytes.NewReader(data)) с корректным JSON, но забыли SetConfigType. Что произойдёт?

- ✅ viper вернёт ошибку Unsupported Config Type: формат потока неизвестен
- viper распознает JSON по фигурным скобкам и прочитает всё
- Первые ключи прочитаются, вложенные — нет
- ReadConfig молча вернёт nil и пустой реестр

Формат viper не угадывает: у потока нет расширения, configType пуст — парсеру нечего выбрать, и ReadConfig возвращает ошибку Unsupported Config Type. Пара SetConfigType плюс ReadConfig обязательна в этом порядке.

### Что выведет чтение из строки?

```go
payload := []byte("{"app": {"name": "billing", "port": 8080}}")

viper.SetConfigType("json")
viper.ReadConfig(bytes.NewReader(payload))

fmt.Println(viper.GetString("app.name"), viper.GetInt("app.port"))
```

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

- ✅ billing 8080
- billing 0: числа из JSON приходят строками
- паника: файл конфига не задан
- пустая строка и 0: ReadConfig не заполняет реестр

ReadConfig кладёт распарсенное дерево в тот же реестр, что и ReadInConfig, — файл не нужен. app.port приходит числом, GetInt возвращает 8080, а GetString вытащит вложенный ключ через точку. Проверьте локально: код запускается без единого файла на диске.

### Соберите поиск конфига

Допишите: имя без расширения, путь поиска, подсказку формата для файла без расширения и сам вызов чтения.

```go
viper.___("config")
viper.___("./configs")
viper.___("yaml") // файл configs/config без расширения

err := viper.___()
```

Подсказка: Имя задаёт SetConfigName, каталог — AddConfigPath, формат без расширения подсказывает SetConfigType, читает конфиг ReadInConfig.

Правильные ответы в пропусках: `SetConfigName`, `AddConfigPath`, `SetConfigType`, `ReadInConfig`

### Соберите загрузку конфига по строкам

Расставьте строки: имя конфига, два пути поиска, подсказка формата, чтение с обработкой ошибки и вывод значения.

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

1. `viper.SetConfigName("config")`
2. `viper.AddConfigPath("./configs")`
3. `viper.AddConfigPath(".")`
4. `viper.SetConfigType("yaml")`
5. `if err := viper.ReadInConfig(); err != nil { log.Fatal(err) }`
6. `fmt.Println(viper.GetString("app.name"))`

### Карточки: форматы и поиск

- **SupportedExts** — Фиксированный порядок расширений поиска в viper: json, toml, yaml, yml, properties, props, prop, hcl, tfvars, dotenv, env, ini. Первый существующий файл выигрывает — json в одной папке сильнее yaml.
- **SetConfigName** — Имя файла без расширения: расширение viper подберёт сам по SupportedExts. Работает в паре с AddConfigPath.
- **AddConfigPath** — Добавляет каталог в поиск. Пути проверяются в порядке добавления, внутри каждого каталога — расширения по SupportedExts.
- **SetConfigFile** — Явный путь к файлу, поиск отключается. Формат определяется по расширению; для файла без расширения нужен SetConfigType.
- **SetConfigType и ReadConfig** — Обязательная пара для чтения конфига из io.Reader — строка, stdin, расшифрованный секрет. Без типа вернётся Unsupported Config Type.
- **ConfigFileNotFoundError** — Ошибка ReadInConfig, когда ни один путь не дал файла. Штатный сценарий для программы на дефолтах. Битый синтаксис — другая ошибка: конфиг не грузится целиком.

### Что дальше

Форматы разобраны: файл найден, реестр заполнен — осталось вынуть из него значения, и здесь начинается самое интересное. Семейство Get с приведением типов, нулевыми значениями вместо ошибок и ловушкой any — тема [следующего урока о чтении значений](/courses/viper/lessons/04-chtenie-get/). Примеры запускаются локальным Go, и один опыт стоит сделать обязательно: положите в configs/ конфиг одного формата, затем рядом второго и по дампу настроек убедитесь, кто победил — двухминутная демонстрация порядка SupportedExts запоминается лучше любого текста. Полный маршрут по курсу — на странице [курса viper](/courses/viper/).

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

- viper читает yaml, json, toml, hcl, ini, properties, javaproperties, dotenv и tfvars одним API: парсер выбирается по расширению, код чтения не меняется.
- Поиск без SetConfigFile идёт по путям из AddConfigPath в порядке добавления, а внутри каталога — по SupportedExts: json сильнее toml, toml сильнее yaml.
- Два конфига разных форматов в одной папке — источник недельной отладки: viper берёт первый по списку и не предупреждает.
- SetConfigFile задаёт файл явно и отключает поиск; для файла без расширения добавьте SetConfigType.
- ReadConfig принимает любой io.Reader — строку, stdin, расшифрованный секрет; без SetConfigType вернётся Unsupported Config Type.
- ConfigFileNotFoundError — штатная ситуация для программы на дефолтах; ошибка парсинга валит запуск целиком, и это правильное поведение.
- Выбор формата — вопрос редактора: yaml для людей, json для машин, toml в cargo-стиле, ini для legacy; один формат на каталог — минимум сюрпризов.

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

Соберите проект с папкой configs и загрузкой через SetConfigName. Положите туда config.yaml, затем рядом config.json с другим значением порта. Запустите программу и по дампу настроек убедитесь, что победил json. После этого переключите загрузку на SetConfigFile и повторите прогон — теперь формат за вами, а не за порядком расширений.

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

### Какие форматы viper поддерживает по умолчанию — парсеры подключать отдельно не нужно?

Все базовые форматы — yaml, json, toml, hcl, ini, properties, javaproperties, dotenv, env, tfvars — уже внутри библиотеки, тянуть зависимости не требуется. Формат выбирается по расширению файла или через SetConfigType. Удалённые хранилища вроде etcd и Consul — единственный случай с особыми билд-тегами, но это тема одного из поздних уроков курса.

### Можно ли заставить viper предпочесть yaml, если рядом лежит json?

Порядок SupportedExts через API не настраивается: json проверяется раньше yaml всегда. Рабочие варианты: убрать конфликтующий файл, разложить файлы по разным каталогам с отдельными AddConfigPath или задать точный путь через SetConfigFile — тогда поиск не участвует вовсе.

### ReadInConfig вернул ошибку — файл не нашёлся или он битый?

Проверяйте тип через errors.As: viper.ConfigFileNotFoundError означает, что поиск прошёл впустую, и программа может стартовать на дефолтах. Любая другая ошибка — парсинг: файл найден, синтаксис битый, конфиг не загружен целиком, продолжать запуск с ней нельзя.
