Avatar ·

Как заставить JSON Marshal в Go выводить ключи в нижнем регистре?

Всем привет! Я пишу свой первый REST API на Go и столкнулся с проблемой: при сериализации структуры в JSON через `json.Marshal` все поля выводятся с заглавными буквами, потому что в Go экспортируемые поля начинаются с большой буквы. Например: ```go type User struct { Name string Email string Age int } func main() { u := User{"Иван", "ivan@example.com", 30} data, _ := json.Marshal(u) fmt.Println(string(data)) // Вывод: {"Name":"Иван","Email":"ivan@example.com","Age":30} } ``` Но на фронтенде (React) все ожидают ключи в camelCase или snake_case: `name`, `email`, `age`. Пробовал писать свой маршаллер, но это слишком громоздко для проекта с 20+ структурами. Я читал про `json:"name"` теги, но вручную прописывать их для каждого поля — это же ад, особенно если полей много. Есть ли способ автоматически преобразовывать все ключи в нижний регистр? Или хотя бы как правильно использовать теги, чтобы не сойти с ума? Подскажите, пожалуйста, как это делается в продакшене. Может быть, есть какая-то библиотека или стандартный подход? Заранее спасибо! <<>>

Правильное решение: используйте теги структуры (struct tags)

В Go нет встроенного способа автоматически преобразовывать все имена полей в нижний регистр при маршаллинге, и это сделано намеренно — язык требует явного контроля над сериализацией. Однако есть элегантное и стандартное решение: теги полей структуры.

Теги — это строковые метаданные, которые добавляются после типа поля в обратных кавычках. Для JSON используется ключ json. Вот как это работает:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func main() {
    u := User{"Иван", "ivan@example.com", 30}
    data, _ := json.Marshal(u)
    fmt.Println(string(data))
    // Вывод: {"name":"Иван","email":"ivan@example.com","age":30}
}

Дополнительные возможности тегов

  • Пропуск пустых полей: json:"name,omitempty" — если поле пустое (zero value), оно не будет включено в JSON.
  • Игнорирование поля: json:"-" — поле полностью исключается из сериализации/десериализации.
  • Строковое представление чисел: json:"age,string" — число будет сериализовано как строка (например, "30" вместо 30).
  • Вложенные структуры: теги работают рекурсивно для всех полей.

Как не сойти с ума при 20+ структурах

Вот несколько практических советов:

  • Используйте IDE с автодополнением: GoLand, VS Code с плагином Go или vim-go умеют генерировать теги. В VS Code можно выделить структуру и выполнить команду "Go: Add Tags".
  • Автоматическая генерация: Популярные инструменты:
    • gomodifytags — утилита для массового добавления/изменения тегов.
    • golangci-lint с плагином tagliatelle — проверяет соответствие тегов заданному стилю (snake_case, camelCase и т.д.).
  • Единый стиль: Договоритесь в команде использовать snake_case для JSON (как в большинстве REST API) или camelCase. Я рекомендую snake_case, так как он читаемее в URL и заголовках.

Пример с реальной структурой

type Order struct {
    ID          int64     `json:"id"`
    UserID      int64     `json:"user_id"`
    TotalAmount float64   `json:"total_amount,string"`
    CreatedAt   time.Time `json:"created_at"`
    Items       []Item    `json:"items,omitempty"`
}

type Item struct {
    ProductID int64   `json:"product_id"`
    Quantity  int     `json:"quantity"`
    Price     float64 `json:"price"`
}

Альтернативы (когда теги не подходят)

Если вы не можете изменять исходный код структур (например, они из внешней библиотеки), можно реализовать свой маршаллер через json.Marshaler интерфейс или использовать промежуточное преобразование в map[string]interface{} с ручным приведением ключей к нижнему регистру. Но это менее эффективно и рекомендуется только как крайняя мера.

Итог: теги — это стандартный, производительный и поддерживаемый способ. Потратьте 10 минут на их расстановку через IDE, и проблема будет решена раз и навсегда.

<<>>

Автоматическое преобразование через кастомный MarshalJSON

Если вы не хотите писать теги для каждой структуры вручную, можно реализовать общий подход через интерфейс json.Marshaler. Например, создать вспомогательную функцию, которая рекурсивно преобразует любую структуру в map[string]interface{} с ключами в нижнем регистре:

import (
    "encoding/json"
    "reflect"
    "strings"
)

func LowercaseMarshal(v interface{}) ([]byte, error) {
    val := reflect.ValueOf(v)
    if val.Kind() == reflect.Ptr {
        val = val.Elem()
    }
    
    result := make(map[string]interface{})
    typ := val.Type()
    
    for i := 0; i < val.NumField(); i++ {
        field := typ.Field(i)
        key := strings.ToLower(field.Name)
        result[key] = val.Field(i).Interface()
    }
    
    return json.Marshal(result)
}

// Использование:
type User struct {
    Name  string
    Email string
    Age   int
}

func main() {
    u := User{"Иван", "ivan@example.com", 30}
    data, _ := LowercaseMarshal(u)
    fmt.Println(string(data))
    // {"name":"Иван","email":"ivan@example.com","age":30}
}

Недостатки этого подхода

  • Теряется поддержка тегов json (omitempty, string).
  • Не работает с вложенными структурами без доработки.
  • Меньшая производительность из-за рефлексии.
  • Не поддерживает кастомные типы с собственным MarshalJSON.

Этот метод подходит для прототипов или случаев, когда структуры простые и плоские. Для продакшена всё же рекомендую использовать теги — они дают больше контроля и лучше документируют API.