jsontypedef / json-typedef-codegen

A CLI tool that generates code from JSON Typedef schemas
https://jsontypedef.com/docs/tools/jtd-codegen
MIT License
154 stars 31 forks source link

Certain type names causes errors for Python codegen #81

Open gtker opened 1 year ago

gtker commented 1 year ago

Having user defined types with names like Optional will make Python unable to use the typing.Optional type since it is from imported.

Not importing typing.Optional but instead using it with the full module prefix would fix this.

The same would happen with Enum, Any, or List.

Shame that this is unmaintained, I would not mind making a PR for this since it's a simple fix. Creating this issue so that others will realize their issue faster.


With the following schema:

{
    "properties": {
        "opt": {
            "ref": "optional"
        }
    },
    "optionalProperties": {
        "t": {"type":"string"}
    },
    "definitions": {
        "optional": {}
    }
}

Running jtd-codegen --python-out . schema.json creates the following:

# Code generated by jtd-codegen for Python v0.3.1

import re
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, Optional, Union, get_args, get_origin

@dataclass
class Schema:
    opt: 'Optional'
    t: 'Optional[str]'

    @classmethod
    def from_json_data(cls, data: Any) -> 'Schema':
        return cls(
            _from_json_data(Optional, data.get("opt")),
            _from_json_data(Optional[str], data.get("t")),
        )

    def to_json_data(self) -> Any:
        data: Dict[str, Any] = {}
        data["opt"] = _to_json_data(self.opt)
        if self.t is not None:
             data["t"] = _to_json_data(self.t)
        return data

@dataclass
class Optional:
    value: 'Any'

    @classmethod
    def from_json_data(cls, data: Any) -> 'Optional':
        return cls(_from_json_data(Any, data))

    def to_json_data(self) -> Any:
        return _to_json_data(self.value)

def _from_json_data(cls: Any, data: Any) -> Any:
    if data is None or cls in [bool, int, float, str, object] or cls is Any:
        return data
    if cls is datetime:
        return _parse_rfc3339(data)
    if get_origin(cls) is Union:
        return _from_json_data(get_args(cls)[0], data)
    if get_origin(cls) is list:
        return [_from_json_data(get_args(cls)[0], d) for d in data]
    if get_origin(cls) is dict:
        return { k: _from_json_data(get_args(cls)[1], v) for k, v in data.items() }
    return cls.from_json_data(data)

def _to_json_data(data: Any) -> Any:
    if data is None or type(data) in [bool, int, float, str, object]:
        return data
    if type(data) is datetime:
        return data.isoformat()
    if type(data) is list:
        return [_to_json_data(d) for d in data]
    if type(data) is dict:
        return { k: _to_json_data(v) for k, v in data.items() }
    return data.to_json_data()

def _parse_rfc3339(s: str) -> datetime:
    datetime_re = '^(\d{4})-(\d{2})-(\d{2})[tT](\d{2}):(\d{2}):(\d{2})(\.\d+)?([zZ]|((\+|-)(\d{2}):(\d{2})))$'
    match = re.match(datetime_re, s)
    if not match:
        raise ValueError('Invalid RFC3339 date/time', s)

    (year, month, day, hour, minute, second, frac_seconds, offset,
     *tz) = match.groups()

    frac_seconds_parsed = None
    if frac_seconds:
        frac_seconds_parsed = int(float(frac_seconds) * 1_000_000)
    else:
        frac_seconds_parsed = 0

    tzinfo = None
    if offset == 'Z':
        tzinfo = timezone.utc
    else:
        hours = int(tz[2])
        minutes = int(tz[3])
        sign = 1 if tz[1] == '+' else -1

        if minutes not in range(60):
            raise ValueError('minute offset must be in 0..59')

        tzinfo = timezone(timedelta(minutes=sign * (60 * hours + minutes)))

    second_parsed = int(second)
    if second_parsed == 60:
        second_parsed = 59

    return datetime(int(year), int(month), int(day), int(hour), int(minute),
                    second_parsed, frac_seconds_parsed, tzinfo)            

Running it with

import model
import json
data = json.loads("{}")
t = model.Schema.from_json_data(data)

causes

Traceback (most recent call last):
  File "/media/asd/96B4-13F2/jtdc/t.py", line 7, in <module>
    t = model.Schema.from_json_data(data)
  File "/media/asd/96B4-13F2/jtdc/model.py", line 18, in from_json_data
    _from_json_data(Optional[str], data.get("t")),
TypeError: 'type' object is not subscriptable