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
33 changes: 6 additions & 27 deletions src/mcpadapt/utils/modeling.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from copy import deepcopy
from typing import Any, Dict, ForwardRef, List, Optional, Type, Union

import jsonref
from pydantic import BaseModel, Field, create_model
import re

json_type_mapping: dict[str, Type] = {
"string": str,
Expand All @@ -13,31 +13,10 @@
}


def resolve_refs_and_remove_defs(json_obj):
# Extract $defs
defs = json_obj.get("$defs", {})

# Function to recursively resolve $ref
def _resolve(obj):
if isinstance(obj, dict):
if "$ref" in obj:
ref_path = obj["$ref"]
match = re.match(r"#/\$defs/(\w+)", ref_path)
if match:
def_key = match.group(1)
return _resolve(deepcopy(defs.get(def_key, {})))
return {k: _resolve(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [_resolve(i) for i in obj]
else:
return obj

json_obj = _resolve(json_obj)

# Remove $defs
json_obj.pop("$defs", None)

return json_obj
def resolve_refs_and_remove_defs(json_obj: dict[str, Any]) -> dict[str, Any]:
"""Resolve JSON Schema $ref pointers (including in-document paths) and strip $defs."""
resolved = jsonref.replace_refs(json_obj, proxies=False)
return {key: value for key, value in resolved.items() if key != "$defs"}


def create_model_from_json_schema(
Expand Down
40 changes: 39 additions & 1 deletion tests/utils/test_modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
Tests for the modeling module, specifically focused on JSON Schema handling.
"""

from mcpadapt.utils.modeling import create_model_from_json_schema
from mcpadapt.utils.modeling import (
create_model_from_json_schema,
resolve_refs_and_remove_defs,
)


def test_direct_modeling_with_list_type():
Expand Down Expand Up @@ -35,3 +38,38 @@ def test_direct_modeling_with_list_type():
# Verify the model works as expected with number
instance = model(multi_type_field=42)
assert instance.multi_type_field == 42


def test_create_model_with_property_path_ref():
"""Regression for #75: in-document $ref paths must be resolved before modeling."""
schema = {
"type": "object",
"properties": {
"requestResponse": {
"type": "object",
"properties": {
"request": {
"type": "object",
"properties": {
"filename": {"type": "string"},
"body": {"type": "string"},
},
"required": ["body"],
"additionalProperties": False,
},
"response": {
"$ref": "#/properties/requestResponse/properties/request"
},
},
"required": ["request", "response"],
"additionalProperties": False,
}
},
"additionalProperties": False,
}

model = create_model_from_json_schema(resolve_refs_and_remove_defs(schema))

generated_schema = model.model_json_schema()
assert "requestResponse" in generated_schema["properties"]
assert "$ref" not in str(generated_schema)