datamodel-code-generator icon indicating copy to clipboard operation
datamodel-code-generator copied to clipboard

Feature request: timedelta support

Open jesse-r-s-hines opened this issue 2 years ago • 3 comments

Is your feature request related to a problem? Please describe.

We have a lot of timedelta fields in our schemas and would like to take advantage of Pydantic's native handling of timedelta types. However, datamodel-code-generator does not appear to support outputting timedelta objects. Native timedelta support would be a very useful addition.

Pydantic handles timedeltas natively. It also appears that msgspec added timedelta support recently (see https://github.com/jcrist/msgspec/pull/475) so I don't think there would be anything blocking adding timedelta support to all datamodel-code-generator output types.

Describe the solution you'd like I've seen two common JSON schema formats to represent timedeltas:

  • This one is used by Pydantic 1 and serializes the timedeltas as floats of seconds

    {
        "type": "object",
        "properties": {
          "granularity": {
            "type": "number",
            "format": "time-delta"
          }
        },
        "required": ["granularity" ]
    }
    
  • This one is used by Pydantic 2 and serializes the timedeltas as ISO 8106 duration strings

    {
        "type": "object",
        "properties": {
          "granularity": {
            "type": "string",
            "format": "duration",
            "title": "Granularity"
          }
        },
        "required": [
          "granularity"
        ]
    }
    

Ideally, datamodel-code-generator would understand and output timedelta fields for both of those schemas.

Describe alternatives you've considered Leaving the current default behavior and having {"type": "number", "format": "time-delta"} fall back to just a float of seconds.

jesse-r-s-hines avatar Oct 18 '23 21:10 jesse-r-s-hines

also having this problem and came across this issue - I prepared this snippet which might be useful for debugging / creating a test if this feature does get implemented:

import typing as ty
import importlib
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from datamodel_code_generator import InputFileType, generate, DataModelType
from pydantic import BaseModel
import sys

def pydantic_model_from_json_schema(json_schema: str) -> ty.Type[BaseModel]:
    load = json_schema["title"] if "title" in json_schema else "Model"

    with TemporaryDirectory() as temporary_directory_name:
        temporary_directory = Path(temporary_directory_name)
        file_path = "model.py"
        module_name = file_path.split(".")[0]
        output = Path(temporary_directory / file_path)
        generate(
            json.dumps(json_schema),
            input_file_type=InputFileType.JsonSchema,
            input_filename="example.json",
            output=output,
            output_model_type=DataModelType.PydanticV2BaseModel,
        )
        spec = importlib.util.spec_from_file_location(module_name, output)
        module = importlib.util.module_from_spec(spec)
        sys.modules[module_name] = module
        spec.loader.exec_module(module)
    return getattr(module, load)

schema = {
        "title": "Test",
        "type": "object",
        "properties": {
            "a_int": {
                "default": 1,
                "title": "A Int",
                "type": "integer"
            },
            "i_duration": {
                "default": "PT2H33M3S",
                "format": "duration",
                "title": "I Duration",
                "type": "string"
            }
        },
    }

Model = pydantic_model_from_json_schema(schema)
Model.model_fields

Model = pydantic_model_from_json_schema(schema)
Model.model_fields
#> {'a_int': FieldInfo(annotation=Union[int, NoneType], required=False, default=1, title='A Int'),
#>  'i_duration': FieldInfo(annotation=Union[str, NoneType], required=False, default='PT2H33M3S', title='I Duration')}

this is functionality that I require so I made this hack, to modify the generated pydantic model:

from datetime import timedelta
from pydantic import create_model


def get_timedelta_fields(schema: dict) -> list[str]:
    pr = schema["properties"]
    return [k for k, v in pr.items() if "format" in v and v["format"] == "duration"]
     
def update_timedelta_field(model: BaseModel, timedelta_fields: list[str]) -> BaseModel:
    """returns a new pydantic model where serialization validators have been added to dates,
    datetimes and durations for compatibility with excel"""
    get_default = lambda obj: obj.default if hasattr(obj, "default") else ...
    deltas = {
        k: (timedelta, get_default(v))
        for k, v in model.model_fields.items()
        if k in timedelta_fields
    } | {"__base__": model}
    return create_model(model.__name__ + "New", **deltas)


li = get_timedelta_fields(schema)
Model1 = update_timedelta_field(Model, li)
Model1.model_fields
#> {'a_int': FieldInfo(annotation=Union[int, NoneType], required=False, default=1, title='A Int'),
#> 'i_duration': FieldInfo(annotation=timedelta, required=False, default='PT2H33M3S')}

thanks for this great package!

jgunstone avatar Jun 01 '24 12:06 jgunstone