Several actions fail request parsing before the HTTP call with "Invalid request data provided / locationId is required" — even though locationId is sent. In HighLevel this breaks 8 actions (GET_PRODUCTS, SEARCH_CONVERSATIONS, SEARCH_DUPLICATE_CONTACT, GET_EMAILS_CAMPAIGNS_EMAILS, LIST_FUNNELS_FUNNEL, GET_FUNNELS_PAGE_COUNT, GET_CALENDAR, GET_EVENT_NOTIFICATION_2).
Tools are expected to accept both camelCase and snake_case argument names — every RequestWrapper.parse() runs normalize_keys_to_model() to map either form onto the model field. But _get_model_field_info (mercury/utils/key_normalization.py) only inspected field_info.alias, never field_info.validation_alias.
So for a required field declared with only validation_alias="locationId" and no populate_by_name=True:
locationId isn't recognised → the normalizer rewrites the key to the snake_case field name location_id;location_id, because a validation_alias field binds only to its alias unless populate_by_name=True.Both input forms fail. There was also an internal inconsistency: schema.py:_build_field_name_to_alias_map already mirrors Pydantic's validation_alias(str) > alias resolution, but the normalizer did not.
Resolve the effective alias in _get_model_field_info the same way schema.py / Pydantic schema generation does — a plain-string validation_alias takes precedence over alias. An AliasChoices (non-str) validation_alias falls back to alias, so its behaviour is unchanged.
| Field declaration | before | after |
|---|---|---|
plain alias | both forms parse | unchanged |
validation_alias (str), no PBN | both forms FAIL | both forms parse |
validation_alias + populate_by_name | both forms parse | still both parse |
validation_alias = AliasChoices | as-is | unchanged (falls back to alias) |
The change is strictly more permissive — it cannot make a previously-passing parse fail.
Fixes the 8 HighLevel actions plus ~150 latent validation_alias cases across 20+ other apps (constant_contact, dropbox, roam, zendesk, …).
TestAliasResolutionMatrix unit test asserts all four alias declarations accept both camelCase and snake_case through the real RequestWrapper.tests/test_apps/test_highlevel/test_request_parsing.py) asserts no top-level required field is rejected by its published schema key.tests/test_utils/{test_key_normalization,test_param_name_sanitization,test_schema}.py pass.validation_alias-using apps.tests/test_attrs failure count identical before/after (pre-existing, unrelated).