Como não empacotar uma estrutura vazia em JSON com Go?

88

Eu tenho uma estrutura como esta:

type Result struct {
    Data       MyStruct  `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

Mas mesmo se a instância de MyStruct estiver totalmente vazia (ou seja, todos os valores são padrão), ela está sendo serializada como:

"data":{}

Eu sei que os documentos de codificação / json especificam que os campos "vazios" são:

falso, 0, qualquer ponteiro nulo ou valor de interface e qualquer matriz, fatia, mapa ou string de comprimento zero

mas sem considerar uma estrutura com todos os valores vazios / padrão. Todos os seus campos também são marcados com omitempty, mas isso não tem efeito.

Como posso fazer com que o pacote JSON não empacote meu campo que é uma estrutura vazia?

Matt
fonte

Respostas:

137

Como dizem os documentos, "qualquer ponteiro nulo". - torna a estrutura um ponteiro. Os ponteiros têm valores óbvios "vazio": nil.

Fix - defina o tipo com um campo de ponteiro de estrutura :

type Result struct {
    Data       *MyStruct `json:"data,omitempty"`
    Status     string    `json:"status,omitempty"`
    Reason     string    `json:"reason,omitempty"`
}

Então, um valor como este:

result := Result{}

Organizará como:

{}

Explicação: Observe o *MyStructem nossa definição de tipo. A serialização JSON não se importa se é um ponteiro ou não - isso é um detalhe de tempo de execução. Portanto, transformar campos de estrutura em ponteiros só tem implicações para a compilação e o tempo de execução).

Apenas observe que se você alterar o tipo de campo de MyStructpara *MyStruct, precisará de ponteiros para valores de estrutura para preenchê-lo, como:

Data: &MyStruct{ /* values */ }
Matt
fonte
2
Deus te abençoe Matt, isso é o que eu estava procurando
Venkata SSKM Chaitanya
@Matt, você tem certeza de que &MyStruct{ /* values */ }conta como um ponteiro nulo? O valor não é nulo.
Shuzheng
@Matt É possível tornar esse comportamento padrão? Quero omitir sempre o vazio. (basicamente não usar a tag em todos os campos de todos os structs)
Mohit Singh
17

Como @chakrit mencionado em um comentário, você não pode chegar a este trabalho através da implementação json.Marshalerde MyStruct, e implementar uma função de triagem JSON personalizado em cada struct que usa-lo pode ser muito mais trabalho. Realmente depende do seu caso de uso se vale a pena o trabalho extra ou se você está preparado para viver com estruturas vazias em seu JSON, mas aqui está o padrão que uso aplicado a Result:

type Result struct {
    Data       MyStruct
    Status     string   
    Reason     string    
}

func (r Result) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    }{
        Data:   &r.Data,
        Status: r.Status,
        Reason: r.Reason,
    })        
}

func (r *Result) UnmarshalJSON(b []byte) error {
    decoded := new(struct {
        Data     *MyStruct   `json:"data,omitempty"`
        Status   string      `json:"status,omitempty"`
        Reason   string      `json:"reason,omitempty"`
    })
    err := json.Unmarshal(b, decoded)
    if err == nil {
        r.Data = decoded.Data
        r.Status = decoded.Status
        r.Reason = decoded.Reason
    }
    return err
}

Se você tem structs enormes com muitos campos, isso pode se tornar tedioso, especialmente alterar a implementação de um struct mais tarde, mas sem reescrever todo o jsonpacote para atender às suas necessidades (não é uma boa ideia), essa é praticamente a única maneira que consigo pensar em conseguir isso é feito enquanto mantém um não-ponteiro MyStructlá.

Além disso, você não precisa usar estruturas embutidas, você pode criar estruturas nomeadas. Eu uso LiteIDE com autocompletar de código, então eu prefiro inline para evitar confusão.

Leylandski
fonte
9

Dataé uma estrutura inicializada, portanto, não é considerada vazia porque encoding/jsonolha apenas para o valor imediato, não os campos dentro da estrutura.

Infelizmente, retornar nilde json.Marhsleratualmente não funciona:

func (_ MyStruct) MarshalJSON() ([]byte, error) {
    if empty {
        return nil, nil // unexpected end of JSON input
    }
    // ...
}

Você poderia dar Resultum empacotador também, mas não vale a pena o esforço.

A única opção, como sugere Matt, é fazer Dataum ponteiro e definir o valor como nil.

Lucas
fonte
1
Não vejo por encoding/json que não posso verificar os campos filho da estrutura. Não seria muito eficiente, sim. Mas certamente não é impossível.
nemo
@nemo Eu entendo seu ponto, mudei o texto. Não funciona porque não seria eficiente. No json.Marshalerentanto, isso pode ser feito caso a caso.
Lucas
2
É não possível decidir wether ou não MyStructestá vazio , implementando um json.Marshalerem MyStructsi. Prova: play.golang.org/p/UEC8A3JGvx
chakrit
Para fazer isso, você terá que implementar json.Marshalerno Resultpróprio tipo de conteúdo , o que pode ser muito inconveniente.
chakrit
3

Há uma excelente proposta da Golang para esse recurso que está ativa há mais de 4 anos, portanto, neste ponto, é seguro presumir que ele não fará parte da biblioteca padrão tão cedo. Como @Matt apontou, a abordagem tradicional é converter os structs em ponteiros para structs . Se essa abordagem for inviável (ou impraticável), uma alternativa é usar um codificador json alternativo que suporte a omissão de estruturas de valor zero .

Eu criei um espelho da biblioteca Golang json ( clarketm / json ) com suporte adicionado para omitir estruturas de valor zero quando a omitemptytag é aplicada. Essa biblioteca detecta o zeroness de maneira semelhante ao popular codificador YAML go-yaml , verificando recursivamente os campos de estrutura pública .

por exemplo

$ go get -u "github.com/clarketm/json"
import (
    "fmt"
    "github.com/clarketm/json" // drop-in replacement for `encoding/json`
)

type Result struct {
    Data   MyStruct `json:"data,omitempty"`
    Status string   `json:"status,omitempty"`
    Reason string   `json:"reason,omitempty"`
}

j, _ := json.Marshal(&Result{
    Status: "204",
    Reason: "No Content",
})

fmt.Println(string(j))
// Note: `data` is omitted from the resultant json.
{
  "status": "204"
  "reason": "No Content"
}
Travis Clarke
fonte