Skip to content
Draft
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
17 changes: 14 additions & 3 deletions adala/utils/pydantic_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ class Person(BaseModel):
# `description` is the model class docstring
model_description = json_schema.get("description", "")

required_fields = set(json_schema.get("required", []))
fields_def = {}
for name, prop in json_schema.get("properties", {}).items():
fields_def[name] = json_schema_to_pydantic_field(prop)
fields_def[name] = json_schema_to_pydantic_field(
prop, required=name in required_fields
)

# Create the BaseModel class using create_model().
model = create_model(model_name, **fields_def)
Expand All @@ -63,13 +66,16 @@ class Person(BaseModel):
return model


def json_schema_to_pydantic_field(json_schema: Dict[str, Any]) -> Tuple[Any, Field]:
def json_schema_to_pydantic_field(
json_schema: Dict[str, Any], required: bool = True
) -> Tuple[Any, Field]:
"""
Converts a JSON schema property to a Pydantic field definition.

Args:
name: The field name.
json_schema: The JSON schema property.
required: Whether the containing object requires this property.

Returns:
A Pydantic field definition.
Expand All @@ -95,8 +101,10 @@ def json_schema_to_pydantic_field(json_schema: Dict[str, Any]) -> Tuple[Any, Fie
if constraint in json_schema:
field_params[constraint] = json_schema[constraint]

default = ... if required else json_schema.get("default", None)

# Create a Field object with the type and optional parameters.
return type_, Field(..., **field_params)
return type_, Field(default, **field_params)


def json_schema_to_pydantic_type(
Expand Down Expand Up @@ -195,6 +203,9 @@ class Template(BaseModel):
"title": class_name,
"description": description,
"properties": field_schema,
# Skill response fields have historically all been required. Keep that
# contract while the general JSON Schema converter honors `required`.
"required": list(field_schema),
}

return json_schema_to_model(json_schema)
56 changes: 56 additions & 0 deletions tests/test_pydantic_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"enum": ["engineer", "doctor", "teacher"],
},
},
"required": ["name", "age", "profession"],
}


Expand Down Expand Up @@ -59,6 +60,61 @@ def test_json_schema_to_model():
assert instance.profession == expected_instance.profession


def test_json_schema_to_model_honors_required_fields():
GeneratedModel = json_schema_to_model(
{
"type": "object",
"title": "Record",
"properties": {
"required_name": {"type": "string"},
"optional_note": {"type": "string"},
"priority": {"type": "integer", "default": 3},
},
"required": ["required_name"],
}
)

assert GeneratedModel.model_fields["required_name"].is_required()
assert not GeneratedModel.model_fields["optional_note"].is_required()
assert not GeneratedModel.model_fields["priority"].is_required()

instance = GeneratedModel(required_name="task")
assert instance.optional_note is None
assert instance.priority == 3
assert instance.model_dump(exclude_unset=True) == {"required_name": "task"}

from pydantic import ValidationError

with pytest.raises(ValidationError):
GeneratedModel(required_name="task", optional_note=None)


def test_json_schema_to_model_honors_nested_required_fields():
GeneratedModel = json_schema_to_model(
{
"type": "object",
"properties": {
"profile": {
"type": "object",
"properties": {
"name": {"type": "string"},
"note": {"type": "string"},
},
"required": ["name"],
}
},
}
)

assert GeneratedModel().profile is None
assert GeneratedModel(profile={"name": "Ada"}).profile.note is None

from pydantic import ValidationError

with pytest.raises(ValidationError):
GeneratedModel(profile={"note": "missing name"})


@pytest.mark.parametrize(
"field_schema, fields_descriptions, good_params, bad_params, ",
(
Expand Down
Loading