👻

【Go】エンコード・デコード周りをこっそりとチラ見🫣

2022/08/07に公開

クライアントや外部APIと連携するとき。
様々なフォーマットでリクエストしたりレスポンスを受け取ったりすると思います。

標準パッケージのencoding/jsonはよく使われることでしょう。

https://pkg.go.dev/encoding/json

例えば、以下のように構造体にjsonタグを貼り付けてですね...

type User struct {
	Name     string    `json:"name"`
	BirthDay time.Time `json:"birthDay"`
}

json.Unmarshal関数を使って、

jsonStr := `{"name": "Wakaba Seisei", "birthDay": "1997-03-04T02:47:01+09:00"}`

var row User
if err := json.Unmarshal([]byte(jsonStr), &row); err != nil {
	fmt.Println(err)
}

// output: 1997-03-04 02:47:01 +0900 +0900
fmt.Printf("%v", row.BirthDay)

JSONで渡されたデータをGoの構造体に落とし込むことは往々にしてありそうです。
(ちなみに、このようなデコードの場合だと、フィールドとJSONのキーの大文字・小文字の違い関係なく、よしなにマッピングしてくれるので、必須ではないです。エンコードの場合だとタグをつけないと、大文字のフィールドに合わせてJSONのキーも大文字になってしまいます。)

しかし、birthDayを"1997-03-04T02:47:01+09:00"ではなく、"1997-03"として受け取ろうとすると、パースで失敗してしまいます。

parsing time "\"1997-03\"" as "\"2006-01-02T15:04:05Z07:00\"": cannot parse "\"" as "-"

逆に、構造体からJSONにエンコードするときには、"1997-03-04T02:47:01+09:00"というフォーマットではなくて、"1997-03"というフォーマットで表したいこともあると思います。ただ、jsonタグをぺぺぺっと貼り付けただけでは、実現できなそうです。

Goのencoding/jsonには、インターフェースとしてMarshalerとUnmarshalerが用意されているため、それを実装している型を以下のように用意することで、JSONの変換をカスタマイズすることができます。

package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type User struct {
	Name     string `json:"name"`
	BirthDay Month  `json:"birthDay"`
}

type Month time.Time

const (
	monthFormat = "2006-01"
)

// UnmarshalJSON JSONから構造体にデコードしたい場合
func (month *Month) UnmarshalJSON(data []byte) error {
	if string(data) == "null" {
		return nil
	}
	var str string
	if err := json.Unmarshal(data, &str); err != nil {
		return err
	}

	tm, err := time.Parse(monthFormat, str)
	if err != nil {
		return err
	}
	*month = Month(tm.In(time.UTC))
	return nil
}

// MarshalJSON 構造体からJSONにエンコードする場合
func (month Month) MarshalJSON() ([]byte, error) {
	if time.Time(month).IsZero() {
		return []byte("null"), nil
	}

	return json.Marshal(time.Time(month).Format(monthFormat))
}

func main() {
	jsonStr := `{"name": "Wakaba Seisei", "birthDay": "1997-03"}`

	var row User
	if err := json.Unmarshal([]byte(jsonStr), &row); err != nil {
		fmt.Println(err)
	}
	
	// output: 1997-03-01 00:00:00 +0000 UTC
	fmt.Printf("%v", time.Time(row.BirthDay))
	
	user := User{Name: "Wakaba Seisei", BirthDay: Month(time.Date(1997, 3, 4, 0, 0, 0, 0, time.Local))}

	jsonData, err := json.Marshal(user)
	if err != nil {
		fmt.Println(err)
	}
	
	// output: {"name":"Wakaba Seisei","birthDay":"1997-03"}
	fmt.Printf("%s", jsonData)
}

Discussion