Удаление полей из структуры или скрытие их в ответе JSON


181

Я создал API в Go, который после вызова выполняет запрос, создает экземпляр структуры и затем кодирует эту структуру как JSON перед отправкой обратно вызывающей стороне. Теперь я бы хотел, чтобы вызывающая сторона могла выбрать конкретные поля, которые они хотели бы вернуть, передав параметр GET «fields».

Это означает, что в зависимости от значений полей моя структура изменится. Есть ли способ удалить поля из структуры? Или, по крайней мере, динамически скрывать их в ответе JSON? (Примечание: иногда у меня есть пустые значения, поэтому тег JSON omitEmpty здесь не будет работать) Если ни один из них не возможен, есть ли предложение по лучшему способу обработки этого? Заранее спасибо.

Ниже приведена уменьшенная версия структур, которые я использую:

type SearchResult struct {
    Date        string      `json:"date"`
    IdCompany   int         `json:"idCompany"`
    Company     string      `json:"company"`
    IdIndustry  interface{} `json:"idIndustry"`
    Industry    string      `json:"industry"`
    IdContinent interface{} `json:"idContinent"`
    Continent   string      `json:"continent"`
    IdCountry   interface{} `json:"idCountry"`
    Country     string      `json:"country"`
    IdState     interface{} `json:"idState"`
    State       string      `json:"state"`
    IdCity      interface{} `json:"idCity"`
    City        string      `json:"city"`
} //SearchResult

type SearchResults struct {
    NumberResults int            `json:"numberResults"`
    Results       []SearchResult `json:"results"`
} //type SearchResults

Затем я закодирую и выведу ответ примерно так:

err := json.NewEncoder(c.ResponseWriter).Encode(&msg)

7
@Jacob, согласно обновленному ответу PuerkitoBio, я думаю, что вы неправильно прочитали вопрос. Принятый (в настоящее время) может не быть «правильным ответом» на ваш вопрос, но тот, который задан здесь! (В настоящее время) ответ с наибольшим количеством голосов может ответить на ваш вопрос, но совершенно не применим к этому!
Дейв С

Ответы:


277

РЕДАКТИРОВАТЬ: я заметил несколько отрицательных голосов и еще раз взглянуть на этот вопрос и ответы. Кажется, что большинство людей упускают из виду, что ОП просил динамически выбирать поля на основе списка полей, предоставленного вызывающим абонентом. Вы не можете сделать это с помощью статически определенного тега структуры json.

Если вы хотите всегда пропустить поле для json-кодирования, тогда, конечно, используйте его, json:"-"чтобы игнорировать поле (также обратите внимание, что это не требуется, если ваше поле не экспортировано - эти поля всегда игнорируются кодером json). Но это не вопрос ОП.

Цитировать комментарий к json:"-"ответу:

Этот [ json:"-"ответ] является ответом, который большинство людей, заканчивающих здесь поиском, захочет, но это не ответ на вопрос.


В этом случае я бы использовал интерфейс map [string] {} вместо struct. Вы можете легко удалить поля, вызвав deleteвстроенные на карту поля для удаления.

То есть, если вы не можете запросить только запрашиваемые поля в первую очередь.


5
Скорее всего, вы не хотите полностью отбрасывать определение типа. Это будет утомительно, например, когда вы захотите написать другие методы этого типа, которые обращаются к этим полям. Использование промежуточного звена map[string]interface{}имеет смысл, но не требует, чтобы вы отбрасывали определение типа.
Джорелли

1
Другой ответ - фактический ответ на этот вопрос.
Джейкоб

1
Возможным недостатком удаления является то, что иногда вам может потребоваться поддержка нескольких представлений json вашей структуры (карты). Например, представление json для клиента без чувствительного поля и представление json для базы данных с чувствительным полем. К счастью, все еще возможно использовать структуру - просто посмотрите на мой ответ.
Адам Куркевич

Это работает для меня, так как мне нужны были только конкретные, Idно я не хочу возвращать всю структуру json. Спасибо за это!
Луи Миранда

155

используйте `json:" - "`

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

документ: http://golang.org/pkg/encoding/json/#Marshal


14
Я не согласен с @Jacob, потому что ОП сказал, что они хотят динамически управлять полями вывода на основе записей строки запроса к API. Например, если вызывающая сторона API запрашивает только отрасль и страну, вам нужно будет удалить все остальное. Вот почему «помеченный» ответ помечается как ответ на этот вопрос. Этот высоко оцененный ответ предназначен для маркировки полей, которые явно никогда не будут доступны любому встроенному json-marshaler - НИКОГДА. если вы хотите его динамически, ответ будет отмечен галочкой.
eduncan911

12
Это ответ, который захочет получить большинство людей, заканчивающих здесь поиск, но это не ответ на вопрос.
Филипп Хаглунд

5
Как уже говорилось, ФП запрашивал метод динамического формирования DTO.
codepushr

54

Другой способ сделать это - иметь структуру указателей с ,omitemptyтегом. Если указатели равны нулю , поля не будут маршаллическими.

Этот метод не потребует дополнительного отражения или неэффективного использования карт.

Тот же пример, что и у jorelli, использующего этот метод: http://play.golang.org/p/JJNa0m2_nw


3
+1 Полностью согласен. Я все время использую это правило / трюк со встроенными маршалерами (и даже построил средство чтения / записи CSV на основе этого правила! - я могу открыть этот исходный код, как только появится еще один пакет csv go). OP может просто не установить значение * Country на nil, и оно будет опущено. И замечательно, что вы также предоставили хороший набор play.golang.
eduncan911

2
Конечно, этот метод требует отражения, маршалинг json-to-struct stdlib всегда использует отражение (фактически он всегда использует период отражения, карту или структуру или что-то еще).
Мн

Да, но это не требует дополнительного отражения с помощью интерфейсов, которые рекомендуют некоторые другие ответы.
Друска

14

Вы можете использовать reflectпакет для выбора нужных полей, отражая теги полей и выбирая jsonзначения тегов. Определите метод на вашем SearchResults типа , который выбирает нужные поля и возвращают их в виде map[string]interface{}, а затем маршал , что вместо того , чтобы в SearchResults STRUCT себя. Вот пример того, как вы можете определить этот метод:

func fieldSet(fields ...string) map[string]bool {
    set := make(map[string]bool, len(fields))
    for _, s := range fields {
        set[s] = true
    }
    return set
}

func (s *SearchResult) SelectFields(fields ...string) map[string]interface{} {
    fs := fieldSet(fields...)
    rt, rv := reflect.TypeOf(*s), reflect.ValueOf(*s)
    out := make(map[string]interface{}, rt.NumField())
    for i := 0; i < rt.NumField(); i++ {
        field := rt.Field(i)
        jsonKey := field.Tag.Get("json")
        if fs[jsonKey] {
            out[jsonKey] = rv.Field(i).Interface()
        }
    }
    return out
}

и вот работоспособное решение, которое показывает, как вы будете вызывать этот метод и упорядочить свой выбор: http://play.golang.org/p/1K9xjQRnO8


если подумать, вы можете разумно обобщить шаблон selectfields на любой тип и любой ключ тега; в этом нет ничего конкретного для определения SearchResult или ключа json.
Джорелли

Я пытаюсь держаться подальше от рефлексии, но это довольно хорошо сохраняет информацию о типах ... Приятно иметь код, который документирует, как ваши структуры выглядят лучше, чем набор тегов if / else в методе validate () (если вы даже есть один)
Актау

7

Я только что опубликовал шериф , который преобразует структуры в карту на основе тегов, помеченных на полях структуры. Затем вы можете выполнить маршал (JSON или другие) сгенерированной карты. Вероятно, он не позволяет вам сериализовать только набор полей, запрошенных вызывающей стороной, но я предполагаю, что использование набора групп позволит вам охватить большинство случаев. Использование групп вместо полей напрямую, скорее всего, также увеличит возможности кеширования.

Пример:

package main

import (
    "encoding/json"
    "fmt"
    "log"

    "github.com/hashicorp/go-version"
    "github.com/liip/sheriff"
)

type User struct {
    Username string   `json:"username" groups:"api"`
    Email    string   `json:"email" groups:"personal"`
    Name     string   `json:"name" groups:"api"`
    Roles    []string `json:"roles" groups:"api" since:"2"`
}

func main() {
    user := User{
        Username: "alice",
        Email:    "alice@example.org",
        Name:     "Alice",
        Roles:    []string{"user", "admin"},
    }

    v2, err := version.NewVersion("2.0.0")
    if err != nil {
        log.Panic(err)
    }

    o := &sheriff.Options{
        Groups:     []string{"api"},
        ApiVersion: v2,
    }

    data, err := sheriff.Marshal(o, user)
    if err != nil {
        log.Panic(err)
    }

    output, err := json.MarshalIndent(data, "", "  ")
    if err != nil {
        log.Panic(err)
    }
    fmt.Printf("%s", output)
}

7

Возьмите три ингредиента:

  1. reflectПакет перебрать всех полей структуры.

  2. ifЗаявление , чтобы забрать поля , которые вы хотите Marshal, и

  3. encoding/jsonПакет Marshalполей по своему вкусу.

Приготовление:

  1. Смешайте их в хорошей пропорции. Используйте reflect.TypeOf(your_struct).Field(i).Name()для получения имени iполя your_struct.

  2. Используйте, reflect.ValueOf(your_struct).Field(i)чтобы получить типовое Valueпредставление поля ith your_struct.

  3. Использование fieldValue.Interface()для получения фактического значения (upcasted к типу интерфейса {}) из fieldValueтипа Value(обратите внимание на использование кронштейна - интерфейс () метод производитinterface{}

Если вам, к счастью, удастся не сжечь транзисторы или автоматические выключатели, вы должны получить что-то вроде этого:

func MarshalOnlyFields(structa interface{},
    includeFields map[string]bool) (jsona []byte, status error) {
    value := reflect.ValueOf(structa)
    typa := reflect.TypeOf(structa)
    size := value.NumField()
    jsona = append(jsona, '{')
    for i := 0; i < size; i++ {
        structValue := value.Field(i)
        var fieldName string = typa.Field(i).Name
        if marshalledField, marshalStatus := json.Marshal((structValue).Interface()); marshalStatus != nil {
            return []byte{}, marshalStatus
        } else {
            if includeFields[fieldName] {
                jsona = append(jsona, '"')
                jsona = append(jsona, []byte(fieldName)...)
                jsona = append(jsona, '"')
                jsona = append(jsona, ':')
                jsona = append(jsona, (marshalledField)...)
                if i+1 != len(includeFields) {
                    jsona = append(jsona, ',')
                }
            }
        }
    }
    jsona = append(jsona, '}')
    return
}

Обслуживание:

служить с произвольной структурой и map[string]boolполями, которые вы хотите включить, например

type magic struct {
    Magic1 int
    Magic2 string
    Magic3 [2]int
}

func main() {
    var magic = magic{0, "tusia", [2]int{0, 1}}
    if json, status := MarshalOnlyFields(magic, map[string]bool{"Magic1": true}); status != nil {
        println("error")
    } else {
        fmt.Println(string(json))
    }

}

Приятного аппетита!


Предупреждение! Если ваши includeFields содержат имена полей, которые не соответствуют фактическим полям, вы получите недопустимый json. Вы были предупреждены.
Адам Куркевич

5

Вы можете использовать теговый атрибут "omitifempty" или сделать необязательные поля указателями и оставить те, которые вы хотите пропустить, неинициализированными.


Это наиболее правильный ответ на вопрос ОП и вариант использования.
user1943442

2
@ user1943442, не это не так; ОП явно упоминает, почему «пустота» неприменима.
Дейв С

2

Я также столкнулся с этой проблемой, сначала я просто хотел специализировать ответы в моем http-обработчике. Моим первым подходом было создание пакета, который копирует информацию о структуре в другую структуру, а затем упорядочивает эту вторую структуру. Я сделал этот пакет, используя рефлексию, поэтому мне никогда не нравился такой подход, и при этом я не был динамически.

Поэтому я решил изменить пакет encoding / json, чтобы сделать это. Функции Marshal, MarshalIndentи (Encoder) Encodeдополнительно получает

type F map[string]F

Я хотел смоделировать JSON полей, которые необходимы для маршалинга, поэтому он только маршалирует поля, которые находятся на карте.

https://github.com/JuanTorr/jsont

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/JuanTorr/jsont"
)

type SearchResult struct {
    Date        string      `json:"date"`
    IdCompany   int         `json:"idCompany"`
    Company     string      `json:"company"`
    IdIndustry  interface{} `json:"idIndustry"`
    Industry    string      `json:"industry"`
    IdContinent interface{} `json:"idContinent"`
    Continent   string      `json:"continent"`
    IdCountry   interface{} `json:"idCountry"`
    Country     string      `json:"country"`
    IdState     interface{} `json:"idState"`
    State       string      `json:"state"`
    IdCity      interface{} `json:"idCity"`
    City        string      `json:"city"`
} //SearchResult

type SearchResults struct {
    NumberResults int            `json:"numberResults"`
    Results       []SearchResult `json:"results"`
} //type SearchResults
func main() {
    msg := SearchResults{
        NumberResults: 2,
        Results: []SearchResult{
            {
                Date:        "12-12-12",
                IdCompany:   1,
                Company:     "alfa",
                IdIndustry:  1,
                Industry:    "IT",
                IdContinent: 1,
                Continent:   "america",
                IdCountry:   1,
                Country:     "México",
                IdState:     1,
                State:       "CDMX",
                IdCity:      1,
                City:        "Atz",
            },
            {
                Date:        "12-12-12",
                IdCompany:   2,
                Company:     "beta",
                IdIndustry:  1,
                Industry:    "IT",
                IdContinent: 1,
                Continent:   "america",
                IdCountry:   2,
                Country:     "USA",
                IdState:     2,
                State:       "TX",
                IdCity:      2,
                City:        "XYZ",
            },
        },
    }
    fmt.Println(msg)
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {

        //{"numberResults":2,"results":[{"date":"12-12-12","idCompany":1,"idIndustry":1,"country":"México"},{"date":"12-12-12","idCompany":2,"idIndustry":1,"country":"USA"}]}
        err := jsont.NewEncoder(w).Encode(msg, jsont.F{
            "numberResults": nil,
            "results": jsont.F{
                "date":       nil,
                "idCompany":  nil,
                "idIndustry": nil,
                "country":    nil,
            },
        })
        if err != nil {
            log.Fatal(err)
        }
    })

    http.ListenAndServe(":3009", nil)
}

Я еще не пробовал, но это выглядит великолепно. Было бы еще лучше, если бы также поддерживали интерфейс Marshaler.
Хагги

1

Сейчас этот вопрос немного устарел, но я столкнулся с той же проблемой некоторое время назад, и, поскольку я не нашел простого способа сделать это, я создал библиотеку для этой цели. Это позволяет легко генерироватьmap[string]interface{} из статической структуры.

https://github.com/tuvistavie/structomap


Теперь вы можете легко сделать это, используя фрагмент кода из моего рецепта.
Адам Куркевич

Фрагмент - это подмножество библиотеки, но главная проблема, связанная с возвратом a, []byteзаключается в том, что он не очень многократно используется: например, нет простого способа добавить поле впоследствии. Поэтому я бы предложил создать map[string]interface{}сериализацию JSON и включить ее в стандартную библиотеку.
Даниэль Перес

1

У меня не было той же проблемы, но похожей. Приведенный ниже код решает и вашу проблему, конечно, если вы не против проблемы производительности. Перед внедрением такого решения в вашей системе, я рекомендую вам изменить структуру, если вы можете. Отправка ответа переменной структуры является чрезмерной. Я полагаю, что структура ответа представляет собой контракт между запросом и ресурсом, и это не должно быть зависимым запросом (я могу сделать ненужные поля пустыми). В некоторых случаях нам приходится реализовывать этот дизайн, если вы считаете, что именно в этих случаях вот ссылка на игру и код, который я использую.

type User2 struct {
    ID       int    `groups:"id" json:"id,omitempty"`
    Username string `groups:"username" json:"username,omitempty"`
    Nickname string `groups:"nickname" json:"nickname,omitempty"`
}

type User struct {
    ID       int    `groups:"private,public" json:"id,omitempty"`
    Username string `groups:"private" json:"username,omitempty"`
    Nickname string `groups:"public" json:"nickname,omitempty"`
}

var (
    tagName = "groups"
)

//OmitFields sets fields nil by checking their tag group value and access control tags(acTags)
func OmitFields(obj interface{}, acTags []string) {
    //nilV := reflect.Value{}
    sv := reflect.ValueOf(obj).Elem()
    st := sv.Type()
    if sv.Kind() == reflect.Struct {
        for i := 0; i < st.NumField(); i++ {
            fieldVal := sv.Field(i)
            if fieldVal.CanSet() {
                tagStr := st.Field(i).Tag.Get(tagName)
                if len(tagStr) == 0 {
                    continue
                }
                tagList := strings.Split(strings.Replace(tagStr, " ", "", -1), ",")
                //fmt.Println(tagList)
                // ContainsCommonItem checks whether there is at least one common item in arrays
                if !ContainsCommonItem(tagList, acTags) {
                    fieldVal.Set(reflect.Zero(fieldVal.Type()))
                }
            }
        }
    }
}

//ContainsCommonItem checks if arrays have at least one equal item
func ContainsCommonItem(arr1 []string, arr2 []string) bool {
    for i := 0; i < len(arr1); i++ {
        for j := 0; j < len(arr2); j++ {
            if arr1[i] == arr2[j] {
                return true
            }
        }
    }
    return false
}
func main() {
    u := User{ID: 1, Username: "very secret", Nickname: "hinzir"}
    //assume authenticated user doesn't has permission to access private fields
    OmitFields(&u, []string{"public"}) 
    bytes, _ := json.Marshal(&u)
    fmt.Println(string(bytes))


    u2 := User2{ID: 1, Username: "very secret", Nickname: "hinzir"}
    //you want to filter fields by field names
    OmitFields(&u2, []string{"id", "nickname"}) 
    bytes, _ = json.Marshal(&u2)
    fmt.Println(string(bytes))

}

1

Я создал эту функцию для преобразования структуры в строку JSON, игнорируя некоторые поля. Надеюсь, это поможет.

func GetJSONString(obj interface{}, ignoreFields ...string) (string, error) {
    toJson, err := json.Marshal(obj)
    if err != nil {
        return "", err
    }

    if len(ignoreFields) == 0 {
        return string(toJson), nil
    }

    toMap := map[string]interface{}{}
    json.Unmarshal([]byte(string(toJson)), &toMap)

    for _, field := range ignoreFields {
        delete(toMap, field)
    }

    toJson, err = json.Marshal(toMap)
    if err != nil {
        return "", err
    }
    return string(toJson), nil
}

Пример: https://play.golang.org/p/nmq7MFF47Gp


0

Вот как я определил свою структуру.

type User struct {
    Username string  `json:"username" bson:"username"`
    Email    string  `json:"email" bson:"email"`
    Password *string `json:"password,omitempty" bson:"password"`
    FullName string  `json:"fullname" bson:"fullname"`
}

И внутри моей функции user.Password = nilне быть маршалом.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.