Skip to content

Instantly share code, notes, and snippets.

@ochaton
Last active March 4, 2026 22:25
Show Gist options
  • Select an option

  • Save ochaton/5f612f1c1ed527fc3c6e6d388b3efc15 to your computer and use it in GitHub Desktop.

Select an option

Save ochaton/5f612f1c1ed527fc3c6e6d388b3efc15 to your computer and use it in GitHub Desktop.
nullable jsons
package test
import (
"encoding/json"
"testing"
"time"
)
func Some[T any](value T) *T {
return &value
}
type Nullable[T any] struct {
has bool
value T
}
func (n Nullable[T]) HasValue() bool {
return n.has
}
func (n Nullable[T]) Value() T {
if !n.has {
panic("Cannot unwrap a null value")
}
return n.value
}
func (n *Nullable[T]) UnmarshalJSON(data []byte) error {
var temp T
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
n.has = true
n.value = temp
return nil
}
type Birth struct {
Date *time.Time
Place *string
}
func (b *Birth) Update(update UpdateBirth) {
if update.Date.HasValue() {
b.Date = update.Date.Value()
}
if update.Place.HasValue() {
b.Place = update.Place.Value()
}
}
func (b *Birth) Equals(other Birth) bool {
if (b.Date == nil) != (other.Date == nil) {
return false
}
if b.Date != nil && other.Date != nil && !b.Date.Equal(*other.Date) {
return false
}
if (b.Place == nil) != (other.Place == nil) {
return false
}
if b.Place != nil && other.Place != nil && *b.Place != *other.Place {
return false
}
return true
}
type UpdateBirth struct {
Date Nullable[*time.Time] `json:"date"`
Place Nullable[*string] `json:"place"`
}
type User struct {
name string
age *int
Birth *Birth
}
func (u User) Equals(other User) bool {
if u.name != other.name {
return false
}
if (u.age == nil) != (other.age == nil) {
return false
}
if u.age != nil && other.age != nil && *u.age != *other.age {
return false
}
if (u.Birth == nil) != (other.Birth == nil) {
return false
}
if u.Birth != nil && other.Birth != nil && !u.Birth.Equals(*other.Birth) {
return false
}
return true
}
func DefaultUser() User {
return User{
name: "John Doe",
age: Some(30),
}
}
type UpdateUser struct {
Name Nullable[string] `json:"name"`
Age Nullable[*int] `json:"age"`
Birth Nullable[*UpdateBirth] `json:"birth"`
}
func (u *User) Update(update UpdateUser) {
if update.Name.HasValue() {
u.name = update.Name.Value()
}
if update.Age.HasValue() {
u.age = update.Age.Value()
}
if update.Birth.HasValue() {
switch {
case update.Birth.Value() == nil:
u.Birth = nil
case u.Birth == nil:
u.Birth = &Birth{}
fallthrough
default:
u.Birth.Update(*update.Birth.Value())
}
}
}
func TestUpdateUser(t *testing.T) {
t.Run("empty", func(t *testing.T) {
emptyJson := []byte(`{}`)
var update UpdateUser
err := json.Unmarshal(emptyJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal empty JSON: %v", err)
}
if update.Name.HasValue() {
t.Errorf("Expected Name to be nil, got %v", update.Name.Value())
}
if update.Age.HasValue() {
t.Errorf("Expected Age to be nil, got %v", update.Age.Value())
}
s := DefaultUser()
s.Update(update)
expected := DefaultUser()
if !s.Equals(expected) {
t.Errorf("Expected user to remain unchanged, got %v", s)
}
})
t.Run("name only", func(t *testing.T) {
nameJson := []byte(`{"name": "Alice"}`)
var update UpdateUser
err := json.Unmarshal(nameJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal name JSON: %v", err)
}
if !update.Name.HasValue() || update.Name.Value() != "Alice" {
t.Errorf("Expected Name to be 'Alice', got %v", update.Name.Value())
}
if update.Age.HasValue() {
t.Errorf("Expected Age to be nil, got %v", update.Age)
}
s := DefaultUser()
s.Update(update)
expected := User{
name: "Alice",
age: Some(30),
}
if !s.Equals(expected) {
t.Errorf("Expected user to be updated with name 'Alice' and age 30, got %+#v", s)
}
})
t.Run("age only", func(t *testing.T) {
ageJson := []byte(`{"age": 25}`)
var update UpdateUser
err := json.Unmarshal(ageJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal age JSON: %v", err)
}
if update.Name.HasValue() {
t.Errorf("Expected Name to be nil, got %v", update.Name.Value())
}
if !update.Age.HasValue() {
t.Errorf("Expected Age to have a value, got nothing")
}
if v := update.Age.Value(); v == nil || *v != 25 {
t.Errorf("Expected Age to be 25, got %v", v)
}
s := DefaultUser()
s.Update(update)
expected := User{
name: "John Doe",
age: Some(25),
}
if !s.Equals(expected) {
t.Errorf("Expected user to be updated with name 'John Doe' and age 25, got %+#v", s)
}
})
t.Run("name and age", func(t *testing.T) {
fullJson := []byte(`{"name": "Bob", "age": 40}`)
var update UpdateUser
err := json.Unmarshal(fullJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal full JSON: %v", err)
}
if !update.Name.HasValue() || update.Name.Value() != "Bob" {
t.Errorf("Expected Name to be 'Bob', got %v", update.Name.Value())
}
if !update.Age.HasValue() {
t.Errorf("Expected Age to have a value, got nothing")
}
if v := update.Age.Value(); v == nil || *v != 40 {
t.Errorf("Expected Age to be 40, got %v", v)
}
s := DefaultUser()
s.Update(update)
expected := User{
name: "Bob",
age: Some(40),
}
if !s.Equals(expected) {
t.Errorf("Expected user to be updated with name 'Bob' and age 40, got %+#v", s)
}
})
t.Run("age is null", func(t *testing.T) {
nullAgeJson := []byte(`{"age": null}`)
var update UpdateUser
err := json.Unmarshal(nullAgeJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal null age JSON: %v", err)
}
if !update.Age.HasValue() { // the value is given, it is null
t.Errorf("Expected Age to be nil, got %+#v", update.Age)
}
s := DefaultUser()
s.Update(update)
expected := User{
name: "John Doe",
age: nil,
}
if !s.Equals(expected) {
t.Errorf("Expected user to be updated with name 'John Doe' and age nil, got %+#v", s)
}
})
t.Run("birth", func(t *testing.T) {
birthJson := []byte(`{"birth": {"date": "1990-01-01T00:00:00Z", "place": "New York"}}`)
var update UpdateUser
err := json.Unmarshal(birthJson, &update)
if err != nil {
t.Fatalf("Failed to unmarshal birth JSON: %v", err)
}
if !update.Birth.HasValue() {
t.Errorf("Expected Birth to have a value, got nothing")
}
if v := update.Birth.Value(); v == nil {
t.Errorf("Expected Birth to be non-nil, got nil")
}
s := DefaultUser()
s.Update(update)
expected := User{
name: "John Doe",
age: Some(30),
Birth: &Birth{Date: Some(time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC)), Place: Some("New York")},
}
if !s.Equals(expected) {
t.Errorf("Expected user to be updated with birth date '1990-01-01T00:00:00Z' and place 'New York', got %+#v", s)
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment