From 884db346e0496c012048ae2cbcfcb3536af6204e Mon Sep 17 00:00:00 2001 From: Nimra Khalid Date: Wed, 29 Jul 2026 18:53:19 +0500 Subject: [PATCH] Fix Sequence[Union[...]] validation only requiring one element to match _is_valid's Sequence[Union[X, Y]] branch flattened the per-element check into a single any() across (value, union_arg) pairs: return any(isinstance(val, union_arg) for val in value for union_arg in union_args) This means the check passes as soon as ONE element matches ONE type in the union, regardless of whether other elements in the sequence match anything at all. E.g. _is_valid(Sequence[Union[str, int]], ["a", 3.14]) returns True even though 3.14 is neither a str nor an int. This is reachable through real user-facing validation: collection.tenants add/remove/update/upsert all validate their `tenants` argument against Sequence[Union[str, Tenant, ...]] (weaviate/collections/tenants/executor.py). A list mixing a valid tenant name/object with something invalid (e.g. an accidental int, or a stray dict) currently passes validation silently and gets sent to the request-building code instead of failing fast with a clear WeaviateInvalidInputError - the exact thing this argument validation exists to prevent. Fixed to require every element to match at least one union member: all(any(...) for val in value), matching the existing (correct) logic for the non-Union Sequence[X] branch two lines below it. Added regression tests in test/collection/test_validator.py - verified they fail against the pre-fix code (3 failures) and pass with the fix. --- test/collection/test_validator.py | 27 ++++++++++++++++++++++++++- weaviate/validator.py | 4 +++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/test/collection/test_validator.py b/test/collection/test_validator.py index 6c7e655c5..247cbbd84 100644 --- a/test/collection/test_validator.py +++ b/test/collection/test_validator.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any, List, Sequence, Union import numpy as np import pandas as pd @@ -37,3 +37,28 @@ def test_validator(inputs: Any, expected: List[Any], error: bool) -> None: _validate_input(_ValidateArgument(expected=expected, name="test", value=inputs)) else: _validate_input(_ValidateArgument(expected=expected, name="test", value=inputs)) + + +@pytest.mark.parametrize( + "inputs,expected,error", + [ + # every element matches one of the union members -> valid + (["a", 1, "b", 2], [Sequence[Union[str, int]]], False), + (["a", "b"], [Sequence[Union[str, int]]], False), + ([1, 2], [Sequence[Union[str, int]]], False), + # one element (a float) matches neither union member -> must be rejected. + # regression test: the old implementation flattened the per-element check + # into a single any() across (value, union_arg) pairs, so it only took one + # element matching one type to pass the whole sequence, even if other + # elements didn't match anything. + (["a", 3.14], [Sequence[Union[str, int]]], True), + ([3.14, "a"], [Sequence[Union[str, int]]], True), + ([1, 3.14], [Sequence[Union[str, int]]], True), + ], +) +def test_validator_sequence_of_union(inputs: Any, expected: List[Any], error: bool) -> None: + if error: + with pytest.raises(WeaviateInvalidInputError): + _validate_input(_ValidateArgument(expected=expected, name="test", value=inputs)) + else: + _validate_input(_ValidateArgument(expected=expected, name="test", value=inputs)) diff --git a/weaviate/validator.py b/weaviate/validator.py index e9d411dae..46923b69c 100644 --- a/weaviate/validator.py +++ b/weaviate/validator.py @@ -56,7 +56,9 @@ def _is_valid(expected: Any, value: Any) -> bool: if len(args) == 1: if get_origin(args[0]) is Union: union_args = get_args(args[0]) - return any(isinstance(val, union_arg) for val in value for union_arg in union_args) + return all( + any(isinstance(val, union_arg) for union_arg in union_args) for val in value + ) else: return all(isinstance(val, args[0]) for val in value) # bool is a subclass of int, so isinstance(True, int) is True. Reject a