Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .changes/unreleased/Bug Fix-20251205-114146.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Bug Fix
body: Fix JSON serialization error when querying metrics that return Decimal values from the dbt Semantic Layer
time: 2025-12-05T11:41:46.988675841Z
9 changes: 6 additions & 3 deletions src/dbt_mcp/semantic_layer/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from collections.abc import Callable
from contextlib import AbstractContextManager
from datetime import date, datetime
from decimal import Decimal
from typing import Any, Protocol

import pyarrow as pa
Expand Down Expand Up @@ -44,15 +45,17 @@ def DEFAULT_RESULT_FORMATTER(table: pa.Table) -> str:
# Convert PyArrow table to list of dictionaries
records = table.to_pylist()

# Custom JSON encoder to handle date/datetime objects
class DateTimeEncoder(json.JSONEncoder):
# Custom JSON encoder to handle date/datetime and Decimal objects
class ExtendedJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime | date):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj)
return super().default(obj)

# Return JSON with records format and proper indentation
return json.dumps(records, indent=2, cls=DateTimeEncoder)
return json.dumps(records, indent=2, cls=ExtendedJSONEncoder)


class SemanticLayerClientProtocol(Protocol):
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/semantic_layer/test_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime as dt
import json
from decimal import Decimal

import pyarrow as pa

Expand Down Expand Up @@ -140,3 +141,31 @@ def test_default_result_formatter_various_numeric_types() -> None:
assert parsed[0]["int_col"] == 1
assert abs(parsed[0]["float_col"] - 1.1) < 0.0001
assert abs(parsed[0]["decimal_col"] - 100.50) < 0.0001


def test_default_result_formatter_with_python_decimal() -> None:
"""Test handling of Python Decimal objects from PyArrow decimal128 columns.

This tests the fix for Decimal JSON serialization where PyArrow decimal128
columns return Python Decimal objects that need special handling in JSON encoding.
"""
# Create a PyArrow table with decimal128 type (which returns Python Decimal objects)
decimal_array = pa.array(
[Decimal("123.45"), Decimal("678.90"), Decimal("0.01")],
type=pa.decimal128(10, 2),
)
table = pa.table(
{
"amount": decimal_array,
"name": pa.array(["a", "b", "c"]),
}
)

# This should not raise "Object of type Decimal is not JSON serializable"
output = DEFAULT_RESULT_FORMATTER(table)
parsed = json.loads(output)

# Verify Decimal values are correctly converted to floats
assert abs(parsed[0]["amount"] - 123.45) < 0.0001
assert abs(parsed[1]["amount"] - 678.90) < 0.0001
assert abs(parsed[2]["amount"] - 0.01) < 0.0001
Loading