-
Notifications
You must be signed in to change notification settings - Fork 36
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
[Question] required behavioral differences between PGV and PV #228
Comments
Actually, this is not limited to |
Hi @ash2k! Thanks for the detailed report. You're running up against a few pathological cases here. I think you're adding msg := &AccessAsProxyAuthorization{
AccessAs: &AccessAsProxyAuthorization_AccessAsAgentAuthorization{ AccessAsAgentAuthorization: nil } },
}
data, _ := proto.Marshal(msg)
out := &cases.Oneof{}
_ = proto.Unmarshal(data, out)
fmt.Println(prototext.Format(out))
// output:
// z: {} I recommend removing the Next, due to inconsistencies of PGV's behaviors around
Your claim "all fields in a oneof are validated" is not true. There are plenty of tests in the conformance suite that would fail if that were the case. It is just behaviorally different than other rules since it applies regardless of if the field is set or not (i.e., it would be contradictory to ignore a Now, regarding the oneof conformance test labeled For your example, I'd suggest removing both All that said, I do think you've surfaces two interesting things we can make a bit better:
|
I actually cannot reproduce this. Using the OneOf from the conformance tests, this test passes on protovalidate HEAD: func TestValidator_MisconstructedOneof(t *testing.T) {
t.Parallel()
val, err := New()
require.NoError(t, err)
msg := &cases.Oneof{
// explicitly set the message field to nil
// message requires that its field is set to true
O: &cases.Oneof_Z{Z: nil},
}
require.NotPanics(t, func() { err = val.Validate(msg) })
valErr := &ValidationError{}
require.ErrorAs(t, err, &valErr)
assert.Equal(t, "bool.const", valErr.Violations[0].GetConstraintId())
} |
Ok, that's what I suspected. However, I think it might still be valuable to somehow support this. I don't insist though, it's fine if that's not supported. The use case for this is validating constructed messages e.g. before marshaling them and sending/persisting/etc them. This is probably Go-specific (I don't know about other languages).
Yes, I'm going to do that. Thanks!
Yes, sorry. I must have misread the code. I've just tested two
Yes, that would have helped a lot. Perhaps the migration tool should print a link to a doc or explain what's going on and what is the difference vs PGV. Thank you! |
This doesn't apply in most other languages, which use an "opaque" API for setting fields via setter methods instead of direct struct value manipulation. In these cases, you cannot set a oneof to an incomplete value like in your example. (It's possible that protobuf-es has similar issues since it also allows direct object access instead of requiring the use of accessor/mutator methods.) There are related issues that might apply to some other languages though, such as a We could consider adding add'l validation rules that are always enforced, i.e. not triggered via a custom option, because they are always indicative of a malformed message. Such messages can't round trip correctly, meaning that serializing them to bytes and then deserializing those bytes will result in a different message (with non-nil, empty messages in place of the nils). |
If a nil message is ignored by |
It's not that it's ignored by serialization but that it gets quietly transformed into an empty message. This could be a source of surprise/confusion and is very likely to be a bug in the code that constructed the message. Perhaps protovalidate isn't the right place for that sort of check, but it seems like it would be nice to provide, at least on an opt-in basis, and also seems like what the user was trying to achieve 🤷 |
@ash2k For all the protovalidate implementations so far, we operate on the proto reflection representation of the message which hides details about misconstructions. The state of the original concrete message is opaque to protovalidate; in particular, protovalidate-go is not presented a message field in a oneof "set to nil," it's presented as set to the empty message. What this means is, without using Go's reflection utilities prior to the rest of the protovalidate logic, we'd currently have no feasible way of checking for these misconstructions that the protobuf library handles fine without panicking, though user-level code might still panic if not using the
@ash2k The migration tool operates at the AST level, so has no sense of messages as a whole, but we can expand the existing migration tool docs to at least point folks to review the required documentation to make sure the semantics match expectations.
@jhump Testing at least repeated fields with nil values in Go, protovalidate handles this case fine, as the proto reflect library also treats these as the empty messages. I suspect maps would behave the same, too. Ultimately, it's up to the protobuf library to decide what is considered valid, and if it's not resulting in a panic or some other unrecoverable error in the reflect library, protovalidate is bound to follow. And if there's any question, the following passes (and passes through protovalidate cleanly): msg := &pb.CelMapOnARepeated{Values: []*pb.CelMapOnARepeated_Value{
{Name: "foo"},
nil,
}}
require.True(t, msg.ProtoReflect().IsValid()) That said, I do think the libraries can be explicit about what happens in these cases on a per implementation basis. Go's behavior feels consistent to me, and I suspect all the upb based runtimes also share similar behavior.
@timostamm Not sure what you mean by ignored. A nil message in a set oneof field, repeated, and (assuming) map value are treated by all the marshaling code in Go as an empty message. It's only "ignored" on singular message fields (which is the expected behavior). Protovalidate cannot even differentiate in the former cases as it's operating against the proto reflect representation of the message (which shows these fields as empty and not nil). |
@rodaine, this isn't entirely true: https://go.dev/play/p/OeTPMpkWgz_p
The msg := &pb.CelMapOnARepeated{Values: []*pb.CelMapOnARepeated_Value{
{Name: "foo"},
nil,
}}
repeatedFieldDescriptor := msg.ProtoReflect().Descriptor().Fields().ByName("values")
require.True(t, msg.ProtoReflect().
Get(repeatedFieldDescriptor).
List().
Get(1).
Message().
IsValid()) // Boom (related: https://go.dev/play/p/OeTPMpkWgz_p)
As demonstrated above, at least in Go, it is feasible.
This seems like a reasonable take, too. While I can see value in some sort of validation to check for these sorts of misconstructed messages, I'm also happy to agree that protovalidate may not be the place for it, since it is really intended to provide custom validation. A check for a misconstructed message (with invalid/nil interior values that should not be nil) probably instead belongs in the core runtime. |
Oh, good catch! Interestingly it's opaque with get access (which is the only way we interact with it in protovalidate), even if it is invalid. Querying every field and value would be silly though. I agree this is better handled by the runtime library. That said, adding the IsValid check everywhere wouldn't be too difficult, just adds overhead that in the vast majority of cases is not relevant. |
Following #228, direct migrators to verify semantics of `required` and `ignore` rules match their expectations.
BTW, I just filed golang/protobuf#1634 |
Yep, I just tested this. Running With the access authorization example, you would get something like:
|
Thanks for the context, I misremembered how oneof is represented 🤦 We will run into related situations with Protobuf-ES. For example, ECMAScript Number is used to represent several numeric Protobuf types, so it's possible to set the value -1 for a I guess most implementations will have similar cases. Maybe it should be left to the protovalidate implementation to offer additional validation for misconstructed messages at runtime (provided that the Protobuf runtime makes this feasible)? |
Description
I'm migrating from PGV and used the migration tool. Source proto:
After automated migration:
protovalidate seem to validate field rules of all fields inside of
oneof
. What's the point? Only the set field should be validated (if one is set, which may not be required). Currently I cannot express what was expressed before, can I?Steps to Reproduce
This is Go-specific, as that's what I'm using. Try validating the following message:
This works fine/as expected with PGV.
Expected Behavior
No error.
Actual Behavior
Screenshots/Logs
N/A
Environment
Possible Solution
Only validate the field that is set inside of
oneof
, don't validate other fields (they are not set, should not be used anyway).Additional Context
I found this proto message in the conformance suite:
protovalidate/proto/protovalidate-testing/buf/validate/conformance/cases/oneofs.proto
Lines 51 to 58 in cbbac69
And this test:
protovalidate/tools/protovalidate-conformance/internal/cases/cases_oneof.go
Lines 86 to 91 in cbbac69
I wonder what is the point here? Current implementation/semantics make it impossible to use a
oneof
with any other field, apart from the one that isrequired=true
. I don't see how this is useful at all.I could remove
required = true
from both fields, but then the test below would pass, but that's not what I want. I want the validator to catch the missing field!The text was updated successfully, but these errors were encountered: