Skip to content

[BUG] unique fails when called on a concatenated list #353

@engnatha

Description

@engnatha

Description

I was attempting to write a message-level validator that confirms fields across two maps were globally unique. My first attempt was to write an expression that turned the fields of interest into a big list and then call unique() on that list. This gives an error about calling unique() on something that is not a list type.

Steps to Reproduce

Write a message validator that concatenates two lists. This can be from either repeated or maps containers. This example is used below in the expected behavior.

syntax = "proto2";

package playground;
	
import "buf/validate/validate.proto";

message Foo {
    optional string name = 1;
}

message FooContainer {
    option (buf.validate.message).cel = {
        id: "globally_unique_names"
        message: "all names in bar and baz must be globally unique"
        expression: "(this.bar.map(k1, this.bar[k1].name) + this.baz.map(k2, this.baz[k2].name)).unique()"
    };

    map<string, Foo> bar = 1;
    map<string, Foo> baz = 2;
}

Expected Behavior

The playground shows that this is valid CEL. Here's an example of the validation correctly showing that all fields are globally unique (ref).

Here's the playground showing the validation failure when fields are not globally unique (ref).

The reproduction that shows this for maps, but it works for repeated fields as well. (ref).

Actual Behavior

A CELEvalError is raised.

site-packages/protovalidate/internal/extra_func.py in cel_unique(val)
    332     if not isinstance(val, celtypes.ListType):
    333         msg = "invalid argument, expected list"
--> 334         raise celpy.CELEvalError(msg)
    335     return celtypes.BoolType(len(val) == len(set(val)))
    336 

CELEvalError: invalid argument, expected list

Screenshots/Logs

Debug logs are very verbose, so those are shared below.

Environment

  • Operating System: Linux
  • Version: Ubuntu 24.04
  • Compiler/Toolchain: GCC 13.3.0
  • Protobuf Compiler & Version: protobuf 5.29.3
  • Protovalidate Version: 0.11.0

Possible Solution

¯\_(ツ)_/¯

I do have a workaround, though.

Additional Context

I was able to write a more complicated validator that ended up using size() instead. Here's a playground example of this filter (ref).

It is slightly less readable and likely more computationally complex due to the extra map call for every element in one of the containers. Our validation is done only at application startup and the input messages are small, so this has not been an issue.

This suggests to me that the translation performed by map is correctly creating a list but that the type checking code in protovalidate is not condition appropriately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions