-
Notifications
You must be signed in to change notification settings - Fork 37
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
[BUG] Inconsistent behavior for required and ignore_empty #115
Labels
Bug
Something isn't working
Comments
Thanks @jhump. The conformance tests are indeed limited in capturing the full breadth of behaviors. |
rodaine
added a commit
that referenced
this issue
Nov 2, 2023
This patch improves behavioral documentation of the `OneOfConstraints.required`, `FieldConstraints.required`, and `FieldConstraints.ignore_empty` standard constraints. The conformance tests have also been expanded to check the behavior for `required` (with `ignore_empty` to follow in a follow up). Local conformance testing has verified that the current `protovalidate-go` behavior matches the expectations of these constraints. Partially addresses: #115
rodaine
added a commit
that referenced
this issue
Nov 6, 2023
… keys/values (#128) Oneof tests were redundant (and contradictory to the behavior we're going for), so removed them in favor of the more exhaustive ones in the ignore_empty suite. Likewise, added tests to ensure the behavior of ignore_empty as applied to repeated items and map keys/values is consistent as well. Relates to #115
rodaine
added a commit
to bufbuild/protovalidate-go
that referenced
this issue
Nov 6, 2023
Changes related to bufbuild/protovalidate#115
rodaine
added a commit
to bufbuild/protovalidate-python
that referenced
this issue
Nov 7, 2023
Brings the python library in conformance with the revised spec around `required` and `ignore_empty` constraints. See bufbuild/protovalidate#115 The two `# type: ignore` comments are required as typeshed does not currently recognize the `has_presence` property on FieldDescriptor class. I may open an upstream patch to add it in (and audit the rest of the protobuf type definitions).
rodaine
added a commit
to bufbuild/protovalidate-cc
that referenced
this issue
Nov 7, 2023
Bringing the `required` and `ignore_empty` constraints into conformance. See bufbuild/protovalidate#115
rodaine
added a commit
to bufbuild/protovalidate-java
that referenced
this issue
Nov 8, 2023
Bringing the `required` and `ignore_empty` constraints into conformance. See bufbuild/protovalidate#115
igor-tsiglyar
pushed a commit
to igor-tsiglyar/protovalidate
that referenced
this issue
Apr 16, 2024
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This issue represents a few different defects:
required
andignore_empty
field constraints. It is unclear how message fields are handled (is an empty message one that is unset/null or one that is a default/empty message instance?) or how this interacts with field presence and explicitly configured default values (which may be non-zero).required
andignore_empty
field constraints. So once a clear specification is provided for these constraints, some implementations will likely need to be updated to comply with the revised spec.In addition to resulting in inconsistency, some of the implementations are also written in a way that will likely not work correctly with Protobuf Editions. In particular, some implementations do not fully lean on the reflection support of the relevant Protobuf runtime, which will correctly consider "features" in source files that use Protobuf Editions. This means that the new representation for things like implicit vs. explicit field presence is likely to confuse the implementation logic.
Below is a summary of how the various runtimes actually implement these constraints:
Go: The Go implementation fully leverages Protobuf reflection to interpret the data and apply these constraints. This is close to an ideal implementation; the only caveat is that some methods in the runtime's reflection API are not well-specified under all conditions, and the implementation is using these methods even for these unclear conditions. This is likely remedied with a few unit tests to make sure the result is expected, even under these conditions.
To be specific, the implementation leans on
FieldDescriptor.HasPresence()
,FieldDescriptor.Default()
, andMessage.Has(FieldDescriptor)
to decide if a value is present/non-empty and enforce therequired
andignore_empty
constraints. The downside is thatFieldDescriptor.Default()
is being used unconditionally, but the docs for that method do not specify its behavior for non-scalar fields (e.g. lists, maps, and messages).Relevant code snippets:
Java: The Java implementation is very similar to the Go implementation in structure and also leverages Protobuf reflection. However, it uses some conditions to clarify the case of empty/default message values. (It considers an explicitly present message value to be "empty" as long as it equals the empty/default message instance.)
Strangely, it does not use the field's default value if it's type is an unsigned integer: it explicitly considers only zero to be empty. This is an awkward inconsistency with the Go implementation and weird "hybrid" between the Go and Python/C++ approaches. (I suspect this may have roots in the fact that the JVM does not actually have unsigned integer types.)
Relevant code snippets:
C++: The C++ implementation has some quirks. For one, instead of using
FieldDescriptor.has_presence()
, it only considers fields with message types and fields in oneofs to be "explicit presence". This is a proto3-specific interpretation and must change to properly support proto2 and editions.Furthermore, this does not use
FieldDescriptor
's accessors for the field's default value. Instead, it is hard-coded to only consider zero values (0 numeric values, empty string/bytes, and false) to be "empty", with regards to theignore_empty
constraint. This is inconsistent with the Go and Java implementations and also leads to the potential paradox that a non-zero default value for a field could be considered an invalid value if explicitly set.Relevant code snippets:
Python: The Python implementation looks almost exactly like the C++ implementation. It does not use
FieldDescriptor.has_presence
orFieldDescriptor.default_value
properties. Instead it has logic similar to the C++ implementation for deciding if a field has explicit presence. Though its implementation is more specific and only considers non-repeated message fields to have explicit presence (this is more correct than the C++ implementation, butFieldDescriptor.has_presence
would still be a marked improvement).Also like C++, it only considers zero values to be empty and does not consider a field's potential non-zero default value.
Different from C++: it will compare the value to the zero/empty value to decide if a
required
field is present. This seems wrong since it implies that an explicitly set field may still be considered absent. It only usesmessage.HasField(name)
when the field is not in a oneof (which seems like it's overly-specific to the awkward implementation detail used in proto3 optional fields).Relevant code snippets:
In general, the implementations should really look much more like one another. Even if we add conformance tests to better detect inconsistencies like above, it would instill greater confidence if the implementation code also looked more uniform, using the same conditions and reflection APIs across all implementations, where possible. (FWIW, the Protobuf runtimes do provide reasonably consistent APIs for reflection, across languages.)
In my opinion, the implementation in the Go runtime is closest to what I would intuitively expect -- that a default value, even if set explicitly, is not checked when
ignore_empty
is true. However, I would argue that the name of this constraint is very confusing since "empty" doesn't intuitively apply to scalar fields at all. I thinkignore_default
would be more clear, especially since it ignores other constraints not only when the field is absent but also when explicitly set to the default value. The only down-sides to the Go implementation is that its unclear how its usage ofFieldDescriptor.Default()
interacts with non-scalar fields. Is an explicitly-set empty message considered "empty"? And, either way, should it? My opinion is that an explicitly set field is not empty, only an absent one. But I can see this interpretation being debatable (even if the constraint were namedignore_default
).Some of the inconsistencies are a bit subtle, such that devising a conformance test to distinguish one's behavior from another might be tricky. But we should at least have conformance tests that cover the following scenarios, which should have teased out issues in the above implementations:
required
constraint as long as it is explicitly set, even if set to its zero or default value. (This would catch potential issues in the Python implementation.)ignore_empty
. (This would catch the inconsistencies between using a field's configured default vs. hard-coding to zero.)The text was updated successfully, but these errors were encountered: