3 ms·
>Fields with scalar types are always present. Even if you don't set them. Did I mention that (at least in proto3) all protobuffers can be zero-initialized with
by nemothekid 8y ago
>Fields with scalar types are always present. Even if you don't set them. Did I mention that (at least in proto3) all protobuffers can be zero-initialized with absolutely no data in them?
I don't this complaint. You have to initialize data with something. proto2 had to "solve" this problem in C/C++/Go by making everything a pointer. How is dealing with null the "sane" case against zero-initialization?
- romed 8y agoScalar primitive fields in proto1/proto2 C++ generated code are not pointers. They are just class member fields returned by accessors (foo()) and their presence is indicated by a presence vector (has_foo()). At least that's what you get if you use Google's proto compiler. The class layout for proto3 is the same (surprise!) but the presence vector is ignored.
- atombender 8y agoTo be fair, in C++ you could use std::optional or something else that's type-safe. Go has its own philosophy about zero values which is controversial, but at least with pointers you do get to express optional values. And it's not like you can't work around the ergonomic awkwardness that arises: type Post struct { Title *string } func (p *Post) GetTitle() (string, bool) { if p.Title == nil { return "", false } return *p.Title, true } func (p *Post) SetTitle(s string) { p.Title = &s } Hardly elegant, but this is Go. Go also run into the same issue when encoding and decoding JSON. Most languages do distinguish between empty string and a missing string, but not Go, which makes it hard to validate anything. I wrote a code generator for JSON Schema [1] recently, which applies validations while deserializing, and has to resort to a rather low-tech trick to do so: type Post struct { Title string `json:"title"` } func (v *Post) UnmarshalJSON(b []byte) error { var raw map[string]interface{}{ if err := json.Unmarshal(b, &raw); err != nil { return err } if _, ok := raw["title"]; !ok { return errors.New("field title: must be set") } type plain Post var p plain if err := json.Unmarshal(b, &p); err != nil { return err } *v = Post(p) return nil } (Clearly there are slightly more refined and faster ways to do this, but not that easily without importing third-party code, which I wanted to avoid.) C and Go are in the minority here. This doesn't come up in languages like Rust, Swift, TypeScript, Nim, Haskell, OCaml, C#, F#, or Java >= 8, all of which have some sort of optional support (algebraic data types or built-in). For example, TypeScript: interface Post { title?: string } Or Rust: struct Post { title: Option<&str> } [1] https://github.com/atombender/go-jsonschema https://github.com/atombender/go-jsonschema
- Vendan 8y agoso, just for fun, here's a way to do that check without double unmarshalling and allocating maps and such for everything under your struct (also does a check for extra fields, but you could pull that out if you want): type Post struct { Title string `json:"title"` } func (v *Post) UnmarshalJSON(b []byte) error { dec := json.NewDecoder(bytes.NewReader(b)) required := map[string]struct{}{ "title": struct{}{}, } tok, err := dec.Token() if err != nil { return err } if d, ok := tok.(json.Delim); !ok || d != '{' { return errors.New("Expected object") } for { tok, err := dec.Token() if err != nil { return err } if d, ok := tok.(json.Delim); ok && d == '}' { break } switch tok { case "title": delete(required, "title") err := dec.Decode(&v.Title) if err != nil { return err } default: (*v) = Post{} return errors.New(fmt.Sprintf("Unexpected field %s", tok)) } } if len(required) > 0 { (*v) = Post{} return errors.New(fmt.Sprintf("Missing %v required fields", len(required))) } return nil }
- atombender 8y agoThanks, that's neat! The challenge here is that I also need to validate values (e.g. support minimum/maximum), not just within structs, but also standalone values, which means an UnmarshalJSON directly on the type. I might end up doing something like your example, though. (Rejecting extra fields is on my list!)
- Vendan 8y agothe json.NewDecoder stuff does still trigger unmarshals of the types in the struct and such: i.e. package main import ( "bytes" "encoding/json" "errors" "fmt" ) func main() { fmt.Println("Hello, playground") var p Post err := json.Unmarshal([]byte(`{"title": "test"}`), &p) fmt.Println(err, p) err = json.Unmarshal([]byte(`{"title2": "test"}`), &p) fmt.Println(err, p) err = json.Unmarshal([]byte(`{}`), &p) fmt.Println(err, p) err = json.Unmarshal([]byte(`{"title": "long test"}`), &p) fmt.Println(err, p) } type Post struct { Title ShortTitle `json:"title"` } func (v *Post) UnmarshalJSON(b []byte) error { dec := json.NewDecoder(bytes.NewReader(b)) required := map[string]struct{}{ "title": struct{}{}, } tok, err := dec.Token() if err != nil { return err } if d, ok := tok.(json.Delim); !ok || d != '{' { return errors.New("Expected object") } for { tok, err := dec.Token() if err != nil { return err } if d, ok := tok.(json.Delim); ok && d == '}' { break } switch tok { case "title": delete(required, "title") err := dec.Decode(&v.Title) if err != nil { return err } default: (*v) = Post{} return errors.New(fmt.Sprintf("Unexpected field %s", tok)) } } if len(required) > 0 { (*v) = Post{} return errors.New(fmt.Sprintf("Missing %v required fields", len(required))) } return nil } type ShortTitle string func (st *ShortTitle) UnmarshalJSON(b []byte) error { var tmpst string err := json.Unmarshal(b, &tmpst) if err != nil { return err } if len(tmpst) > 6 { return errors.New("Title too long!") } *st = ShortTitle(tmpst) return nil }