diff --git a/adala/utils/pydantic_generator.py b/adala/utils/pydantic_generator.py index 52c43e67..5894a16e 100644 --- a/adala/utils/pydantic_generator.py +++ b/adala/utils/pydantic_generator.py @@ -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) @@ -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. @@ -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( @@ -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) diff --git a/tests/test_pydantic_generator.py b/tests/test_pydantic_generator.py index 32589ace..42dc2962 100644 --- a/tests/test_pydantic_generator.py +++ b/tests/test_pydantic_generator.py @@ -21,6 +21,7 @@ "enum": ["engineer", "doctor", "teacher"], }, }, + "required": ["name", "age", "profession"], } @@ -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, ", (