sql.NullInt64 в JSON

# #mysql #json #rest #go

Вопрос:

У меня есть схема базы данных, в которой возраст не является обязательным. Но когда в Go, через мой спокойный интерфейс, я вывожу данные в формате JSON, я получаю очень неприятный ответ, например {"ID":1,"Name":"John","age":{"Int64":0,"Valid":false}}
, для возраста. Я ожидал, что будет пропущено нулевое значение. Что я упускаю?

 // CREATE TABLE users (
//        id INT AUTO_INCREMENT PRIMARY KEY,
//        name VARCHAR(50) NOT NULL,
//        age INT
// );

// Age is optional

type user struct {
    ID   int           `db:"id"`
    Name string        `db:"name"`
    Age  sql.NullInt64 `db:"age" json:"age,omitempty"`
}

func main() {
    u := user{ID: 1, Name: "John"}
    j, _ := json.Marshal(u)
    fmt.Printf("%s", j)
}

// Expected: {"ID":1,"Name":"John"} since age is NULL/empty
 

https://play.golang.org/p/PS1-4Gw9h5u

Ответ №1:

sql.NullInt64 существует, потому что SQL null не может быть представлен как Go int . Это третье состояние, которое не может быть представлено никаким int значением.

Одним из решений этой проблемы было бы представление таких значений SQL , как *int , но для этого потребуется выделение для случаев, когда значения нет null в базе данных, а распределение плохо сказывается на производительности.

Разработчики пакета SQL разработали NullInt64 решение, которое кодирует третье состояние null в качестве дополнительного Valid логического значения. Это не очень хорошее решение, но это лучшее, что мы можем получить.

Я не уверен, что можно написать JSON marshaller, чтобы NullInt64 это работало так, как вы ожидаете.

Все еще существует проблема «третьего состояния» при маршалировании в JSON. С ,omitempty 0 int также будет опущено, так как вы можете отличить 0 от «не существует»/null?

В любом случае, они не писали пользовательский маршаллер NullInt64 , поэтому он просто кодируется как структура, которой он является.

Вы можете создать тип псевдонима для NullInt64 , написать маршаллер JSON для кодирования так, как вы хотите, чтобы JSON (вам нужен псевдоним, потому что вы не можете добавлять методы к типам из других пакетов). Вам также нужно будет выбрать между вашим NullInt64 и sql.NullInt64 .

Ответ №2:

Вы можете использовать пользовательский тип для null int64. Этот тип работает так, как вы ожидали.

Пример использования:

 package main

import (
    "encoding/json"
    "database/sql/driver" 
    "fmt"
    "strconv"
)

type user struct {
    ID   int             `db:"id"`
    Name string          `db:"name"`
    // pointer needed to omitempty work, otherwise output will be "age": null
    Age  *NullInt64 `db:"age" json:"age,omitempty"`
}

func main() {
    u := user{ID: 1, Name: "John"}
    j, _ := json.Marshal(u)
    fmt.Printf("%sn", j)
}

//    Output:
//        {"ID":1,"Name":"John"}

type NullInt64 struct {
    Val     int64
    IsValid bool
}

func NewNullInt64(val interface{}) NullInt64 {
    ni := NullInt64{}
    ni.Set(val)
    return ni
}

func (ni *NullInt64) Scan(value interface{}) error {
    ni.Val, ni.IsValid = value.(int64)
    return nil
}

func (ni NullInt64) Value() (driver.Value, error) {
    if !ni.IsValid {
        return nil, nil
    }
    return ni.Val, nil
}

func (ni *NullInt64) Set(val interface{}) {
    ni.Val, ni.IsValid = val.(int64)
}

func (ni NullInt64) MarshalJSON() ([]byte, error) {
    if !ni.IsValid {
        return []byte(`null`), nil
    }

    return []byte(strconv.FormatInt(ni.Val, 10)), nil
}

func (ni *NullInt64) UnmarshalJSON(data []byte) error {
    if data == nil || string(data) == `null` {
        ni.IsValid = false
        return nil
    }

    val, err := strconv.ParseInt(string(data), 10, 64)
    if err != nil {
        ni.IsValid = false
        return err
    }

    ni.Val = val
    ni.IsValid = true

    return nil
}

func (ni NullInt64) String() string {
    if !ni.IsValid {
        return `<nil>`
    }

    return strconv.FormatInt(ni.Val, 10)
}
 

Комментарии:

1. Почему вы не можете просто работать sql.NullInt64 и не определять свои собственные NullInt64 ? это потому, что это единственный способ создать пользовательский маршаллер JSON?

2. да, Джсон. Маршалер, fmt.Стрингер, также мне нужно время ожидания, поэтому я решил написать все, что мне нужно, в одном месте 🙂