Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions superset/charts/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ class ChartPostSchema(Schema):
query_context = fields.String(
metadata={"description": query_context_description},
allow_none=True,
validate=utils.validate_json,
validate=utils.validate_query_context_metadata,
)
query_context_generation = fields.Boolean(
metadata={"description": query_context_generation_description}, allow_none=True
Expand Down Expand Up @@ -274,7 +274,9 @@ class ChartPutSchema(Schema):
validate=utils.validate_json,
)
query_context = fields.String(
metadata={"description": query_context_description}, allow_none=True
metadata={"description": query_context_description},
allow_none=True,
validate=utils.validate_query_context_metadata,
)
query_context_generation = fields.Boolean(
metadata={"description": query_context_generation_description}, allow_none=True
Expand Down
35 changes: 34 additions & 1 deletion superset/utils/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,37 @@ def validate_json(value: Union[bytes, bytearray, str]) -> None:
try:
json.validate_json(value)
except json.JSONDecodeError as ex:
raise ValidationError("JSON not valid") from ex
error_msg = "JSON not valid"
raise ValidationError(error_msg) from ex


def validate_query_context_metadata(value: Union[bytes, bytearray, str, None]) -> None:
"""
Validator for query_context field to ensure it contains required metadata.

Validates that the query_context JSON contains the required 'datasource' and
'queries' fields needed for chart data retrieval.

:raises ValidationError: if value is not valid JSON or missing required fields
:param value: a JSON string that should contain datasource and queries metadata
"""
if value is None or value == "":
return # Allow None values and empty strings

# Reuse existing JSON validation logic
validate_json(value)

# Parse and validate the structure
parsed_data = json.loads(value)
Comment on lines +70 to +74
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSON value is being parsed twice - once in validate_json() at line 71 and again at line 74. Consider optimizing by parsing once and validating the structure. For example:

try:
    parsed_data = json.loads(value)
except json.JSONDecodeError as ex:
    raise ValidationError("JSON not valid") from ex

if not isinstance(parsed_data, dict):
    # ... rest of validation

This would avoid the redundant parsing operation.

Suggested change
# Reuse existing JSON validation logic
validate_json(value)
# Parse and validate the structure
parsed_data = json.loads(value)
# Parse and validate the structure
try:
parsed_data = json.loads(value)
except json.JSONDecodeError as ex:
error_msg = "JSON not valid"
raise ValidationError(error_msg) from ex

Copilot uses AI. Check for mistakes.

# Validate required fields exist in the query_context
if not isinstance(parsed_data, dict):
error_msg = "Query context must be a valid JSON object"
raise ValidationError(error_msg)

# When query_context is provided (not None), validate it has required fields
required_fields = {"datasource", "queries"}
missing_fields: set[str] = required_fields - parsed_data.keys()
if missing_fields:
fields_str = ", ".join(sorted(missing_fields))
raise ValidationError(f"Query context is missing required fields: {fields_str}")
120 changes: 120 additions & 0 deletions tests/unit_tests/charts/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@
from superset.charts.schemas import (
ChartDataProphetOptionsSchema,
ChartDataQueryObjectSchema,
ChartPostSchema,
ChartPutSchema,
get_time_grain_choices,
)
from superset.utils import json


def test_get_time_grain_choices(app_context: None) -> None:
Expand Down Expand Up @@ -152,3 +155,120 @@ def test_time_grain_validation_with_config_addons(app_context: None) -> None:
}
result = schema.load(custom_data)
assert result["time_grain"] == "PT10M"


def test_chart_post_schema_query_context_validation(app_context: None) -> None:
"""Test that ChartPostSchema validates query_context contains required metadata"""
schema = ChartPostSchema()

# Valid query_context with datasource and queries should pass
valid_query_context = json.dumps(
{
"datasource": {"type": "table", "id": 1},
"queries": [{"metrics": ["count"], "columns": []}],
}
)
valid_data = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": valid_query_context,
}
result = schema.load(valid_data)
assert result["query_context"] == valid_query_context

# None query_context should be allowed (allow_none=True)
none_data = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": None,
}
result = schema.load(none_data)
assert result["query_context"] is None

# Query context missing 'datasource' field should fail
missing_datasource = json.dumps(
{"queries": [{"metrics": ["count"], "columns": []}]}
)
invalid_data_1 = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": missing_datasource,
}
with pytest.raises(ValidationError) as exc_info:
schema.load(invalid_data_1)
assert "query_context" in exc_info.value.messages
assert "datasource" in str(exc_info.value.messages["query_context"])

# Query context missing 'queries' field should fail
missing_queries = json.dumps({"datasource": {"type": "table", "id": 1}})
invalid_data_2 = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": missing_queries,
}
with pytest.raises(ValidationError) as exc_info:
schema.load(invalid_data_2)
assert "query_context" in exc_info.value.messages
assert "queries" in str(exc_info.value.messages["query_context"])

# Query context missing both 'datasource' and 'queries' should fail
empty_query_context = json.dumps({})
invalid_data_3 = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": empty_query_context,
}
with pytest.raises(ValidationError) as exc_info:
schema.load(invalid_data_3)
assert "query_context" in exc_info.value.messages
assert "datasource" in str(exc_info.value.messages["query_context"])
assert "queries" in str(exc_info.value.messages["query_context"])

# Invalid JSON should fail
invalid_json = "not valid json"
invalid_data_4 = {
"slice_name": "Test Chart",
"datasource_id": 1,
"datasource_type": "table",
"query_context": invalid_json,
}
with pytest.raises(ValidationError) as exc_info:
schema.load(invalid_data_4)
assert "query_context" in exc_info.value.messages


def test_chart_put_schema_query_context_validation(app_context: None) -> None:
"""Test that ChartPutSchema validates query_context contains required metadata"""
schema = ChartPutSchema()

# Valid query_context with datasource and queries should pass
valid_query_context = json.dumps(
{
"datasource": {"type": "table", "id": 1},
"queries": [{"metrics": ["count"], "columns": []}],
}
)
valid_data = {
"slice_name": "Updated Chart",
"query_context": valid_query_context,
}
result = schema.load(valid_data)
assert result["query_context"] == valid_query_context

# Query context missing required fields should fail
missing_datasource = json.dumps(
{"queries": [{"metrics": ["count"], "columns": []}]}
)
invalid_data = {
"slice_name": "Updated Chart",
"query_context": missing_datasource,
}
with pytest.raises(ValidationError) as exc_info:
schema.load(invalid_data)
assert "query_context" in exc_info.value.messages
assert "datasource" in str(exc_info.value.messages["query_context"])
Comment on lines +245 to +274
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for ChartPutSchema doesn't include a test case for None query_context, unlike the corresponding ChartPostSchema test (lines 180-188). Since both schemas have allow_none=True, consider adding a test case to verify that None is properly accepted for consistency and completeness. For example:

# None query_context should be allowed (allow_none=True)
none_data = {
    "slice_name": "Updated Chart",
    "query_context": None,
}
result = schema.load(none_data)
assert result["query_context"] is None

Copilot uses AI. Check for mistakes.
Comment on lines +245 to +274
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test for ChartPutSchema only tests one negative case (missing datasource). Consider adding additional negative test cases similar to the ChartPostSchema test to ensure comprehensive validation coverage:

  • Query context missing 'queries' field
  • Query context missing both fields
  • Invalid JSON

This would ensure the validator works consistently across both POST and PUT schemas.

Copilot uses AI. Check for mistakes.
Loading
Loading