-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
verification failure: Clients should error if response is missing required fields #123
Comments
Yes, this is unfortunate. The actual issue is also more general -- since Conjure treats non-optional fields as "required" generally, this behavior should also be enforced when doing a standard unmarshal from JSON into the Conjure-generated struct. I propose the following fix:
As an example, for the type:
The generated unmarshal would be:
|
One concern for the above would be that if any current code that uses Conjure types implicitly depends on supporting missing values, this will clearly break. Since some code uses Conjure types for serialization (and not just REST APIs), this is more of a risk. |
@bmoylan for feedback/thoughts on the above. |
Overall +1. I'm sympathetic to the behavior break but I think we can communicate this out to warn people that required fields are really required. Some notes on the suggested generated code above:
While we're changing this, it might be worth more correctly implementing wire spec items |
Yup, specifically was looking into https://github.com/palantir/conjure/blob/master/docs/spec/wire.md#42-servers-reject-unknown-fields and https://github.com/palantir/conjure/blob/master/docs/spec/wire.md#41-forward-compatible-clients and the best way to handle this. Erroring on extra fields is a bit tricky. The simplest thing is to potentially use a |
Ah or we could use https://golang.org/src/encoding/json/stream.go?s=1132:1175#L32 |
Yup that's a more recent addition, but believe that (or something like it) would be the most correct way to handle this. |
Played around with this a bit and came up with: package pkg
import (
"bytes"
"encoding/json"
"fmt"
"io"
"testing"
"github.com/palantir/pkg/safejson"
"github.com/stretchr/testify/assert"
)
type OriginalConjureStruct struct {
Foo string `json:"foo"`
Bar *string `json:"bar"`
Baz int `json:"baz"`
}
type optionalGeneratedOriginalConjureStruct struct {
Foo *string `json:"foo"`
Bar *string `json:"bar"`
Baz *int `json:"baz"`
}
var allPresent = []byte(`{
"foo":"foo",
"bar":"bar",
"baz":1
}`)
var hasExtra = []byte(`{
"foo":"foo",
"bar":"bar",
"c": "c",
"baz":1
}`)
var missingRequired = []byte(`{
"baz":1
}`)
var missingOptional = []byte(`{
"foo":"foo",
"baz":1
}`)
func TestServer(t *testing.T) {
for _, tc := range []struct {
name string
input []byte
expected *OriginalConjureStruct
}{
{
name: "allPresent",
input: allPresent,
expected: &OriginalConjureStruct{
Foo: "foo",
Bar: str("bar"),
Baz: 1,
},
},
{
name: "missingOptional",
input: missingOptional,
expected: &OriginalConjureStruct{
Foo: "foo",
Baz: 1,
},
},
{
name: "hasExtra",
input: hasExtra,
},
{
name: "missingRequired",
input: missingRequired,
},
} {
t.Run(tc.name, func(t *testing.T) {
actual, err := functionGenerated(bytes.NewReader(tc.input), true)
if tc.expected == nil {
assert.Nil(t, actual)
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, actual, tc.expected)
}
})
}
}
func TestClient(t *testing.T) {
for _, tc := range []struct {
name string
input []byte
expected *OriginalConjureStruct
}{
{
name: "allPresent",
input: allPresent,
expected: &OriginalConjureStruct{
Foo: "foo",
Bar: str("bar"),
Baz: 1,
},
},
{
name: "missingOptional",
input: missingOptional,
expected: &OriginalConjureStruct{
Foo: "foo",
Baz: 1,
},
},
{
name: "hasExtra",
input: hasExtra,
expected: &OriginalConjureStruct{
Foo: "foo",
Bar: str("bar"),
Baz: 1,
},
},
{
name: "missingRequired",
input: missingRequired,
},
} {
t.Run(tc.name, func(t *testing.T) {
actual, err := functionGenerated(bytes.NewReader(tc.input), false)
if tc.expected == nil {
assert.Nil(t, actual)
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, actual, tc.expected)
}
})
}
}
// Generated: functionGenerated
func functionGenerated(r io.Reader, strict bool) (*OriginalConjureStruct, error) {
var optionalA optionalGeneratedOriginalConjureStruct
dec := getDecoder(r, strict)
err := dec.Decode(&optionalA)
if err != nil {
return nil, err
}
if optionalA.Foo == nil {
return nil, fmt.Errorf(`required field "foo" is missing`)
}
if optionalA.Baz == nil {
return nil, fmt.Errorf(`required field "baz" is missing`)
}
return &OriginalConjureStruct{
Foo: *optionalA.Foo,
Bar: optionalA.Bar,
Baz: *optionalA.Baz,
}, nil
}
// We can either generate this or generate 2 versions of the function above
func getDecoder(r io.Reader, strict bool) *json.Decoder {
if strict {
return DecodeStrict(r)
}
return safejson.Decoder(r)
}
// This moves into safejson in some way. Similar to safejson.Decoder
func DecodeStrict(r io.Reader) *json.Decoder {
decoder := safejson.Decoder(r)
decoder.DisallowUnknownFields()
return decoder
}
// Just a helper for the test
func str(s string) *string {
return &s
} It is a similar thought to what you proposed, however it avoids using a custom |
So there are 2 issues here:
If we want to do (1) correctly, then I think that has to be part of the unmarshal, since we would want to catch this even when no server interactions are required (unless we think this is too risky due to backcompat and just want to enforce this for servers) For (2), this does need to be on the server side, and agree using the decoder is the best approach. There is an interesting question of whether if we do (1) in the unmarshal, whether using a custom decoder is honored/propagated through the custom unmarshal. This is something we'd need to investigate -- if that doesn't work, then we may have to use the approach you outlined, or actually hard-code decoder settings as part of the custom unmarshal. It looks like your proposal above is to handle both (1) and (2) only for arguments provided to server code, and to basically generate the with-optional object in the server. That's certainly a valid approach. However, would just want to clarify our thought process around non-optional fields for Conjure structs in the context other than Conjure REST calls -- I think it's more correct to provide enforcement that these values can't be missing in JSON, in which case it would make sense to handle (1) as part of the Unmarshal. |
Yes so I am saying that unless you define the custom decoder in the actual Unmarshal function, it will not work. So something like:
will work. But:
^^ with a strict decoder will not work. I do not want to stick the decoder in the |
Additionally, I have confirmed that overriding
specifically the |
Yeah looks like the general issue is that defining a custom Given that, I'm on-board with the proposal @k-simons posted above that will enforce the "require non-optional fields" behavior on the client and server, and the "reject unknown fields" behavior on server. The once case this doesn't handle is requiring non-optional fields from an unmarshal performed manually from JSON (for example, if JSON is persisted to disk or encoded and decoded in-memory or across non-Conjure clients), but I think that's an acceptable trade-off (and actually makes the backcompat story easier too) |
Makes sense to me! I think using Are we going to have to generate two different structs for the client and server sides due to the divergent Also it's fine to keep these examples short but want to make sure my concerns about the returned error are handled in the eventual implementation. Thanks for taking this on! |
@bmoylan yup noted. Will make sure that
And we should be able to use the same struct. If you look in #123 (comment) (which is the table tests for client/server) they each just call |
@bmoylan @nmiyake have been playing around with this a bit, the
When we Marshal fields with empty values the keys are preserved, which when we receive these bytes, we will see these keys and assume they were set/present with empty values. You could add the Currently the only solution that I have came up with is to
This forces object creation to actually set the fields they need to. There becomes some ambiguity for optionals, a required string field and an optional string field would each be Thoughts? |
What happened?
What did you want to happen?
The text was updated successfully, but these errors were encountered: