Last active
March 4, 2026 22:25
-
-
Save ochaton/5f612f1c1ed527fc3c6e6d388b3efc15 to your computer and use it in GitHub Desktop.
nullable jsons
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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