Skip to content

Commit 85d31db

Browse files
authored
Fix jsonschema validation that fires CustomKeyInConfigDeprecation (#11580)
* Fix detection of additional config property deprecation Previously we were taking the first `key` on the `instance` property of the jsonschema ValidationError. However, this validation error is raised as an "anyOf" violation, which then has sub-errors in its `context` property. To identify the key in violation, we have to find the `additionalProperties` validation in the sub-errors. The key that is an issue can then be parsed from that sub-error. * Refactor key parsing from jsonschema ValidationError messages to single definition * Update handling `additionalProperties` violations to handle multiple keys in violation * Add changelog * Remove guard logic in jsonschemas validation rule that is no longer needed
1 parent d48476a commit 85d31db

File tree

4 files changed

+84
-28
lines changed

4 files changed

+84
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Fixes
2+
body: Ensure the right key is associatd with the `CustomKeyInConfigDeprecation` deprecation
3+
time: 2025-05-02T13:18:22.940373-05:00
4+
custom:
5+
Author: QMalcolm
6+
Issue: "11576"

core/dbt/jsonschemas.py

+36-26
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44
from datetime import date, datetime
55
from pathlib import Path
6-
from typing import Any, Dict, Iterator
6+
from typing import Any, Dict, Iterator, List
77

88
import jsonschema
99
from jsonschema import ValidationError
@@ -57,6 +57,11 @@ def error_path_to_string(error: jsonschema.ValidationError) -> str:
5757
return path
5858

5959

60+
def _additional_properties_violation_keys(error: ValidationError) -> List[str]:
61+
found_keys = re.findall(r"'\S+'", error.message)
62+
return [key.strip("'") for key in found_keys]
63+
64+
6065
def jsonschema_validate(schema: Dict[str, Any], json: Dict[str, Any], file_path: str) -> None:
6166

6267
if not os.environ.get("DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS"):
@@ -69,33 +74,38 @@ def jsonschema_validate(schema: Dict[str, Any], json: Dict[str, Any], file_path:
6974
# Listify the error path to make it easier to work with (it's a deque in the ValidationError object)
7075
error_path = list(error.path)
7176
if error.validator == "additionalProperties":
72-
key = re.search(r"'\S+'", error.message)
77+
keys = _additional_properties_violation_keys(error)
7378
if len(error.path) == 0:
74-
deprecations.warn(
75-
"custom-top-level-key-deprecation",
76-
msg="Unexpected top-level key" + (" " + key.group() if key else ""),
77-
file=file_path,
78-
)
79+
for key in keys:
80+
deprecations.warn(
81+
"custom-top-level-key-deprecation",
82+
msg="Unexpected top-level key" + (" " + key if key else ""),
83+
file=file_path,
84+
)
7985
else:
80-
deprecations.warn(
81-
"custom-key-in-object-deprecation",
82-
key=key.group() if key else "",
83-
file=file_path,
84-
key_path=error_path_to_string(error),
85-
)
86-
elif (
87-
error.validator == "anyOf"
88-
and len(error_path) > 0
89-
and error_path[-1] == "config"
90-
and isinstance(error.instance, dict)
91-
and len(error.instance.keys()) > 0
92-
):
93-
deprecations.warn(
94-
"custom-key-in-config-deprecation",
95-
key=(list(error.instance.keys()))[0],
96-
file=file_path,
97-
key_path=error_path_to_string(error),
98-
)
86+
key_path = error_path_to_string(error)
87+
for key in keys:
88+
deprecations.warn(
89+
"custom-key-in-object-deprecation",
90+
key=key,
91+
file=file_path,
92+
key_path=key_path,
93+
)
94+
elif error.validator == "anyOf" and len(error_path) > 0 and error_path[-1] == "config":
95+
for sub_error in error.context or []:
96+
if (
97+
isinstance(sub_error, ValidationError)
98+
and sub_error.validator == "additionalProperties"
99+
):
100+
keys = _additional_properties_violation_keys(sub_error)
101+
key_path = error_path_to_string(error)
102+
for key in keys:
103+
deprecations.warn(
104+
"custom-key-in-config-deprecation",
105+
key=key,
106+
file=file_path,
107+
key_path=key_path,
108+
)
99109
else:
100110
deprecations.warn(
101111
"generic-json-schema-validation-deprecation",

tests/functional/deprecations/fixtures.py

+10
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@
168168
my_custom_key: "my_custom_value"
169169
"""
170170

171+
multiple_custom_keys_in_config_yaml = """
172+
models:
173+
- name: models_trivial
174+
description: "This is a test model"
175+
deprecation_date: 1999-01-01 00:00:00.00+00:00
176+
config:
177+
my_custom_key: "my_custom_value"
178+
my_custom_key2: "my_custom_value2"
179+
"""
180+
171181
custom_key_in_object_yaml = """
172182
models:
173183
- name: models_trivial

tests/functional/deprecations/test_deprecations.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
duplicate_keys_yaml,
2727
invalid_deprecation_date_yaml,
2828
models_trivial__model_sql,
29+
multiple_custom_keys_in_config_yaml,
2930
)
3031
from tests.utils import EventCatcher
3132

@@ -341,14 +342,43 @@ def models(self):
341342
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
342343
def test_duplicate_yaml_keys_in_schema_files(self, project):
343344
event_catcher = EventCatcher(CustomKeyInConfigDeprecation)
344-
run_dbt(["parse", "--no-partial-parse"], callbacks=[event_catcher.catch])
345+
run_dbt(
346+
["parse", "--no-partial-parse", "--show-all-deprecations"],
347+
callbacks=[event_catcher.catch],
348+
)
345349
assert len(event_catcher.caught_events) == 1
346350
assert (
347351
"Custom key `my_custom_key` found in `config` at path `models[0].config`"
348352
in event_catcher.caught_events[0].info.msg
349353
)
350354

351355

356+
class TestMultipleCustomKeysInConfigDeprecation:
357+
@pytest.fixture(scope="class")
358+
def models(self):
359+
return {
360+
"models_trivial.sql": models_trivial__model_sql,
361+
"models.yml": multiple_custom_keys_in_config_yaml,
362+
}
363+
364+
@mock.patch.dict(os.environ, {"DBT_ENV_PRIVATE_RUN_JSONSCHEMA_VALIDATIONS": "True"})
365+
def test_duplicate_yaml_keys_in_schema_files(self, project):
366+
event_catcher = EventCatcher(CustomKeyInConfigDeprecation)
367+
run_dbt(
368+
["parse", "--no-partial-parse", "--show-all-deprecations"],
369+
callbacks=[event_catcher.catch],
370+
)
371+
assert len(event_catcher.caught_events) == 2
372+
assert (
373+
"Custom key `my_custom_key` found in `config` at path `models[0].config`"
374+
in event_catcher.caught_events[0].info.msg
375+
)
376+
assert (
377+
"Custom key `my_custom_key2` found in `config` at path `models[0].config`"
378+
in event_catcher.caught_events[1].info.msg
379+
)
380+
381+
352382
class TestCustomKeyInObjectDeprecation:
353383
@pytest.fixture(scope="class")
354384
def models(self):
@@ -363,7 +393,7 @@ def test_custom_key_in_object_deprecation(self, project):
363393
run_dbt(["parse", "--no-partial-parse"], callbacks=[event_catcher.catch])
364394
assert len(event_catcher.caught_events) == 1
365395
assert (
366-
"Custom key `'my_custom_property'` found at `models[0]` in file"
396+
"Custom key `my_custom_property` found at `models[0]` in file"
367397
in event_catcher.caught_events[0].info.msg
368398
)
369399

0 commit comments

Comments
 (0)