Видалення полів із структури або приховування їх у JSON Response


181

Я створив API в Go, який після виклику виконує запит, створює екземпляр структури, а потім кодує цю структуру як JSON перед тим, як відправити назад до абонента. Тепер я хотів би дозволити абоненту можливість вибирати конкретні поля, які вони хотіли б повернути, перейшовши в параметр GET "поля".

Це означає, що залежно від значень полів міняє структуру. Чи є спосіб видалити поля з структури? Або принаймні динамічно приховувати їх у відповіді 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, я думаю, що ви неправильно прочитали питання. Прийнята (наразі) може бути не "правильною відповіддю" на ваше запитання, але є такою, яку тут задають! Відповідь (на даний момент) з найбільш високою оцінкою може відповісти на ваше запитання, але цілком непридатна до цього!
Дейв C

Відповіді:


277

EDIT: Я помітив декілька поточних записів і ще раз подивився на це питання. Здається, більшість людей пропускають, що ОП просила динамічно вибирати поля на основі списку полів, наданих абонентом. Ви не можете цього зробити зі статистично визначеним тегом json struct.

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

Щоб процитувати коментар до json:"-"відповіді:

Ця [ json:"-"відповідь] - це відповідь, яку більшість людей, які закінчують пошук, хотіли б, але це не відповідь на питання.


У цьому випадку я використовував би [string] інтерфейс {} замість структури. Ви можете легко видалити поля, зателефонувавши на deleteвбудовану на карті, щоб поля видалити.

Тобто, якщо ви не можете в першу чергу запитувати лише запитувані поля.


5
ви, швидше за все, не хочете повністю викидати визначення свого типу. Це буде турбувати вниз по лінії, як, наприклад, коли ви хочете написати інші методи цього типу, які мають доступ до цих полів. Використання проміжного map[string]interface{}має сенс, але це не вимагає, щоб ви викидали визначення типу.
jorelli

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"`

doc: http://golang.org/pkg/encoding/json/#Marshal


14
Я не погоджуюся @Jacob, оскільки ОП заявила, що хочуть динамічно керувати полями виводу на основі записів рядкових запитів в API. Наприклад, якщо користувач API вимагає лише галузі та країни, то вам потрібно буде видалити решту. Ось чому "галочка" відповідь позначається як відповідь на це питання. Ця високоголосова відповідь призначена для позначення полів, явно недоступних для будь-якого вбудованого-json-маршалера, - EVER. якщо ви хочете, щоб це було динамічно, відповідь поставлена ​​галочкою.
eduncan911

12
Це відповідь, яку хочуть більшість людей, які закінчуються на пошуках, але це не відповідь на питання.
Філіп Хаглунд

5
Як уже зазначалося, ОП просила метод динамічно формувати DTO.
codepushr

54

Інший спосіб зробити це - мати структуру покажчиків з ,omitemptyтегом. Якщо покажчики будуть нульовими , поля не будуть маршировані.

Цей метод не вимагатиме додаткового відображення або неефективного використання карт.

Той самий приклад, як жореллі, використовуючи цей метод: http://play.golang.org/p/JJNa0m2_nw


3
+1 Повністю згоден. Я користуюсь цим правилом / трюком весь час із вбудованими маршалерами (і навіть будував читач / автори CSV, виходячи з цього правила! - Я можу відкрити джерело, що незабаром ще один пакет csv). ОП тоді може просто не встановити нульове значення * Країна, і це буде пропущено. І дивовижно, що ви поставили хороший; y також набрали play.golang.
eduncan911

2
Зрозуміло, що цей метод вимагає рефлексії, імпровізація json-to-структура stdlib завжди використовує відображення (насправді він завжди використовує період відображення, карту чи структуру чи будь-що інше).
mna

Так, але це не потребує додаткового відображення за допомогою інтерфейсів, які рекомендують деякі інші відповіді.
Друська

14

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

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


придумавши це, ви могли б розумно узагальнити шаблон обраного поля до будь-якого типу та будь-якої клавіші тегів; про це немає нічого конкретного для визначення SearchResult або ключа json.
jorelli

Я намагаюся триматися подалі від роздумів, але це дуже добре зберігає інформацію про тип ... Приємно мати код, який документує те, що ваші структури виглядають краще, ніж купу тегів if / else у валідаті () методі (якщо ви навіть є один)
Актау

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()для отримання назви ith-го поля your_struct.

  2. Використовуйте reflect.ValueOf(your_struct).Field(i)для отримання типового Valueподання iполя поля your_struct.

  3. Використовуйте fieldValue.Interface()для отримання фактичного значення (переведеного на тип інтерфейсу {}) fieldValueтипу Value(зверніть увагу на використання дужок - метод Interface () видає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" або зробити вказівники на необов'язкові поля та залишити ті, що потрібно, неініціалізованими.


Це найправильніша відповідь на питання та питання використання ОП.
користувач1943442

2
@ user1943442, не це; ОП прямо зазначає, чому "опустошення" не застосовується.
Дейв C

2

Я також стикався з цією проблемою, спочатку я просто хотів спеціалізувати відповіді на своєму http обробці. Першим моїм підходом було створення пакету, який копіює інформацію структури до іншої структури, а потім маршалює цю другу структуру. Я зробив цей пакет, використовуючи роздуми, тому мені ніколи не сподобався такий підхід, а також я не був динамічним.

Тому я вирішив змінити пакет кодування / 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.
huggie

1

Питання зараз трохи постарене, але я з тим самим питанням натрапив трохи раніше, і, як я не знайшов простий спосіб це зробити, я створив бібліотеку, яка відповідає цій меті. Це дозволяє легко генерувати a map[string]interface{}зі статичної структури.

https://github.com/tuvistavie/structomap


Тепер ви можете легко зробити це за допомогою фрагмента коду з мого рецепту.
Адам Куркевич

Фрагмент - це підмножина бібліотеки, але головна проблема, пов’язана з поверненням, []byteполягає в тому, що це не дуже багаторазове використання, наприклад, не простий спосіб додати поле згодом. Тож я б запропонував створити map[string]interface{}частину серіалізації JSON та дозволити їй частину серіалізації 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.