Разбор API-ответов
В этом руководстве показано, как разбирать типичные JSON-ответы HTTP API с помощью CyberGo JSON: извлечение статуса и метаданных пагинации, обработка массивов срезами пути и десериализация в структуры.
Разбор постраничного API-ответа
Имитируется постраничный REST API-ответ: извлекаются статус и метаданные пагинации, срез items[0:2] получает подмножество, затем извлекаются поля каждого элемента.
package main
import (
"fmt"
"github.com/cybergodev/json"
)
func main() {
// Имитация постраничного API-ответа
apiResponse := `{
"status": "success",
"data": {
"page": 2,
"per_page": 5,
"total": 48,
"items": [
{"id": 6, "name": "Проект 6", "stars": 120},
{"id": 7, "name": "Проект 7", "stars": 89},
{"id": 8, "name": "Проект 8", "stars": 245},
{"id": 9, "name": "Проект 9", "stars": 56},
{"id": 10, "name": "Проект 10", "stars": 312}
]
}
}`
// 1. Извлекаем статус и метаданные пагинации
status := json.GetString(apiResponse, "status")
page := json.GetInt(apiResponse, "data.page")
total := json.GetInt(apiResponse, "data.total")
fmt.Printf("Статус: %s, страница %d, всего %d\n", status, page, total)
// 2. Получаем весь массив данных
items := json.GetArray(apiResponse, "data.items")
fmt.Printf("Элементов на странице: %d\n", len(items))
// 3. Получаем подмножество через срез пути (первые 2 элемента)
firstTwo, err := json.Get(apiResponse, "data.items[0:2]")
if err != nil {
panic(err)
}
fmt.Printf("Первые два: %v\n", firstTwo)
// 4. Перебираем массив, извлекая поля каждого элемента (рекомендуется ForeachWithPath: один разбор, доступ к каждому элементу)
err = json.ForeachWithPath(apiResponse, "data.items", func(key any, item *json.IterableValue) {
fmt.Printf(" - %s (%d stars)\n", item.GetString("name"), item.GetInt("stars"))
})
if err != nil {
panic(err)
}
}
// Вывод:
// Статус: success, страница 2, всего 48
// Элементов на странице: 5
// Первые два: [map[id:6 name:Проект 6 stars:120] map[id:7 name:Проект 7 stars:89]]
// - Проект 6 (120 stars)
// - Проект 7 (89 stars)
// - Проект 8 (245 stars)
// - Проект 9 (56 stars)
// - Проект 10 (312 stars)Подсказка
Синтаксис среза пути [start:end] возвращает подмножество массива; также можно использовать [start:end:step] для среза с шагом, [-1] для последнего элемента и [*] как подстановочный знак по всем элементам. Полный синтаксис см. в Синтаксисе выражений пути.
При переборе массива предпочитайте ForeachWithPath циклу с построением путей (fmt.Sprintf("data.items.%d.name", i) с отдельным запросом на каждый элемент): первый разбирает JSON один раз, а внутри каждого элемента значения получаются напрямую по имени поля, к тому же код выходит короче; во втором каждая строка пути — отдельный независимый запрос.
Однократное получение нескольких полей
Когда в ответе нужно извлечь много полей, GetMultiple разбирает JSON один раз и возвращает значения всех путей (результат — map с путями в качестве ключей) — это экономичнее, чем вызывать Get по отдельности:
package main
import (
"fmt"
"github.com/cybergodev/json"
)
func main() {
apiResponse := `{
"status": "success",
"data": {
"page": 2,
"per_page": 5,
"total": 48,
"items": [
{"id": 6, "name": "Проект 6", "stars": 120},
{"id": 7, "name": "Проект 7", "stars": 89}
]
}
}`
values, err := json.GetMultiple(apiResponse, []string{
"status",
"data.page",
"data.per_page",
"data.total",
"data.items.0.name",
})
if err != nil {
panic(err)
}
fmt.Printf("%s | страница %v/%v, всего %v, первый элемент: %v\n",
values["status"], values["data.page"], values["data.per_page"],
values["data.total"], values["data.items.0.name"])
}
// Вывод: success | страница 2/5, всего 48, первый элемент: Проект 6Примечание
При неудаче любого из путей (не существует или некорректен) GetMultiple возвращает первую ошибку, а соответствующий путь в результирующем map содержит nil. Поэтому он подходит для извлечения из ответов, где поля «обязательно присутствуют»; для необязательных полей используйте GetString(apiResponse, "path", "значение_по_умолчанию") со значением по умолчанию или описанный ниже SafeGet.
SafeGet: безопасный доступ
SafeGet не возвращает error, а возвращает AccessResult: Ok() проверяет существование пути, методы AsInt/AsString и др. выполняют преобразование по мере необходимости, UnwrapOr предоставляет значение по умолчанию — подходит для сторонних ответов с нестабильными типами полей или необязательными полями и никогда не паникует:
package main
import (
"fmt"
"github.com/cybergodev/json"
)
func main() {
apiResponse := `{
"status": 200,
"message": "ok",
"retry_after": "30",
"trace_id": "abc-123"
}`
// status — число (числа JSON разбираются как float64), поле Type сообщает тип времени выполнения
status := json.SafeGet(apiResponse, "status")
fmt.Println("Тип status:", status.Type)
if code, err := status.AsInt(); err == nil {
fmt.Println("Код статуса:", code)
}
// retry_after — секунды в виде строки
retry := json.SafeGet(apiResponse, "retry_after")
if secs, err := retry.AsString(); err == nil {
fmt.Println("Ожидание перед повтором (сек):", secs)
}
// Несуществующий путь: Ok() == false, UnwrapOr даёт значение по умолчанию
deprecated := json.SafeGet(apiResponse, "deprecated_field")
fmt.Println("Устаревшее поле существует:", deprecated.Ok())
fmt.Println("Значение по умолчанию для устаревшего поля:", deprecated.UnwrapOr("none"))
}
// Вывод:
// Тип status: float64
// Код статуса: 200
// Ожидание перед повтором (сек): 30
// Устаревшее поле существует: false
// Значение по умолчанию для устаревшего поля: noneПри неудаче строгого преобразования методы As* возвращают ошибку, а не молча нулевое значение — это не даёт смешивать «поле отсутствует» и «значение равно 0»; если нужно мягкое преобразование (например, строковое представление значения произвольного типа), используйте AsStringConverted.
Десериализация в структуры
Используйте GetTyped[T] для десериализации всего ответа или любого вложенного подобъекта в строго типизированную структуру; используйте ParseAny для получения значения any (удобно, когда структура ответа неизвестна).
package main
import (
"fmt"
"github.com/cybergodev/json"
)
// Repository представляет структуру репозитория в API-ответе
type Repository struct {
ID int `json:"id"`
Name string `json:"name"`
Stars int `json:"stars"`
}
// APIResponse представляет весь API-ответ
type APIResponse struct {
Status string `json:"status"`
Data struct {
Page int `json:"page"`
Total int `json:"total"`
Items []Repository `json:"items"`
} `json:"data"`
}
func main() {
apiResponse := `{
"status": "success",
"data": {
"page": 1,
"total": 3,
"items": [
{"id": 1, "name": "cybergo-json", "stars": 500},
{"id": 2, "name": "cybergo-jwt", "stars": 320},
{"id": 3, "name": "cybergo-httpc", "stars": 280}
]
}
}`
// 1. Десериализуем весь ответ в структуру (путь "." означает корневой объект)
resp := json.GetTyped[APIResponse](apiResponse, ".")
fmt.Printf("Статус: %s, репозиториев: %d\n", resp.Status, resp.Data.Total)
for _, repo := range resp.Data.Items {
fmt.Printf(" #%d %s (%d stars)\n", repo.ID, repo.Name, repo.Stars)
}
// 2. GetTyped для одного вложенного объекта (декодирование подобъекта в структуру)
firstRepo := json.GetTyped[Repository](apiResponse, "data.items.0")
fmt.Printf("Первый репозиторий: %+v\n", firstRepo)
// 3. ParseAny для произвольного значения (когда структура ответа неизвестна)
parsed, err := json.ParseAny(apiResponse)
if err != nil {
panic(err)
}
fmt.Printf("Тип результата: %T\n", parsed)
}
// Вывод:
// Статус: success, репозиториев: 3
// #1 cybergo-json (500 stars)
// #2 cybergo-jwt (320 stars)
// #3 cybergo-httpc (280 stars)
// Первый репозиторий: {ID:1 Name:cybergo-json Stars:500}
// Тип результата: map[string]interface {}Следующие шаги
- Базовые примеры — запросы по пути, основы кодирования и декодирования структур
- Продвинутые примеры — SafeGet, пакетные операции и др.
- Шпаргалка — быстрый справочник по API
- Синтаксис выражений пути — срезы, подстановочные знаки, извлечение полей