AvroSchemaでNullが許可されたデータ型をGo言語の構造体で表現する

ZOZO Advent Calendar 2023 10日目の記事になります。

特殊な例だと思うのですが、今回はAvroSchemaでNullが許可されているデータ構造をGo言語の構造体で表現する方法を紹介します。

AvroShemaについて

AvroShemaはデータのシリアライズフォーマットです。データがバイナリフォーマットされるのでデータサイズが小さい状態で送信できます。 詳しい説明はドキュメントに書かれているので下記ドキュメントをご確認ください。

avro.apache.org

Union型について

Union型はAvroSchemaで定義されている型の一つで、Complex Typesで定義されているデータ型の一つになります。 JSON配列をtypeフィールドで指定できるので複数の型を定義できます。今回はstring型やint型が定義されているPrimitive TypesにNullを許可するために利用します。

avro.apache.org

Nullが許可されたデータの構造例

例としてString型にNull許可された場合のUnion型を定義します。

[
  {
    "name": "animal",
    "doc": "sample",
    "type": [
      "null",
      "string"
    ],
    "default": null
  }
]

上記の定義に対して許可されるJSONのデータ構造はがNullかそうでないかで2パターンあります。 まずデータがNullの場合についてのデータ構造を紹介します。

{
   "animal":null
}

次にデータがNull以外だった場合のデータ構造になります。

{
   "animal":{
      "string":"cat"
   }
}

上記2パターンを見ると、データがNullかそうでないかでJSONの構造が若干変わります。

実装背景

具体的な実装に入る前に、どのような場面での利用を想定しているのか説明します。

データの流れは図の矢印で表しています。Union型はデータの構造が変わるため、Client側で送信するデータ構造を変えずにCloud Pub/SubへAvroSchemaの仕様に沿ったデータを送るような流れを実装します。 Cloud Pub/SubにAvroSchemaの仕様に沿ったデータを送る理由については、Pub/Sub Schemaという機能を利用するためです。Pub/Sub Schemaを利用するとAvroSchemaを用いて送られるデータのValidationを行えます。

cloud.google.com

具体的な実装

実装方針として、まず送られてきたデータをmappingする構造体を定義します。一旦構造体にデータを落とし込んだ後Union型用に定義した構造体へ変換します。

package main

import (
    "encoding/json"
    "fmt"
)

// 受け取ったデータをmappingする構造体
type Animal struct {
    Animal *string `json:"animal,omitempty"`
}

type TransformAnimal struct {
    Animal *StringUnion `json:"animal"`
}

type StringUnion struct {
    StringValue *string `json:"string,omitempty"`
}

func TransformStringUnion(stringValue *string) *StringUnion {
    if stringValue != nil {
        return &StringUnion{
            StringValue: stringValue,
        }
    }
    return nil
}

func main() {
    // nullの場合
    json_data := `
  {
      "animals": null
  }
  `
    nullAnimal := Animal{}
    err := json.Unmarshal([]byte(json_data), &nullAnimal)
    if err != nil {
        panic(err)
    }
    transformedNullAnimal := TransformAnimal{
        Animal: TransformStringUnion(nullAnimal.Animal),
    }
    marshaledTransformedNullAnimal, err := json.Marshal(transformedNullAnimal)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(marshaledTransformedNullAnimal))

    // null以外の場合
    nonNullMessage := `
  {
      "animal": "cat"
  }
  `
    nonNullAnimal := Animal{}
    err = json.Unmarshal([]byte(nonNullMessage), &nonNullAnimal)
    if err != nil {
        panic(err)
    }
    transformNonNullAnimal := TransformAnimal{
        Animal: TransformStringUnion(nonNullAnimal.Animal),
    }
    marshaledTramsformedNonNullAnimal, err := json.Marshal(transformNonNullAnimal)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(marshaledTramsformedNonNullAnimal))
}

go.dev

実際に実行すると、下記出力結果が得られます

// nullの場合
{"animal":null}

// null以外の場合
{"animal":{"string":"cat"}}

まとめ

少し特殊な事例でしたが、Nullが許可されているデータの構造をGo言語の構造体で表現できました。