2

I am using the datamodel-code-generator to generate pydantic models from a JSON schema.

Here is the JSON schema used.

And the generated models after running the datamodel-code-generator.

# File: datamodel.py
from __future__ import annotations
from typing import List
from pydantic import BaseModel

class Record(BaseModel):
    id: int
    name: str

class Table(BaseModel):
    records: List[Record]

class Globals(BaseModel):
    table: Table

I've been trying to extend the generated classes with new attributes.

# File: extensions.py
import json
from datamodel import Table, Globals

class ExtendedTable(Table):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        print('ExtendedTable Constructor')

        # Won't work because "ExtendedTable" object has no field "records_by_id"
        self.records_by_id = {record.id: record for record in self.records}

class ExtendedGlobals(Globals):
    def __init__(self, table: ExtendedTable):
        super().__init__(table=table)
        print('ExtendedGlobals Constructor')

if __name__ == '__main__':
    records = '''
    {
        "table": {
            "records": [{"id": 0, "name": "A"}, {"id": 1, "name": "B"}]
        }
    }
    '''

    content = json.loads(records)

    # Both won't call ExtendedTable.__init__()
    ExtendedGlobals(**content)
    ExtendedGlobals.parse_obj(content)

    ExtendedGlobals(table=ExtendedTable(**content['table']))

However, I haven't found a way to make the Globals class use the extended definition of the table. Also, simply adding new fields to the subclass does not seem to work.

Is there a way to extend these classes without having to modify the pydantic generated models? Or maybe another tool to generate Python code from JSON schema?

1 Answer 1

1

I haven't found a way to make the Globals class use the extended definition of the table

You can change the type of a field in a subclass if you declare the field again using the desired type.

Also, simply adding new fields to the subclass does not seem to work

It looks like you are setting instance attributes in the __init__() method, but fields are declared as class attributes.

This example shows a way to add a calculated field records_by_id to ExtendedTable and use ExtendedTable in ExtendedGlobals:

# File: extensions.py
import json
from typing import Any, Dict, List, Optional

from pydantic import Field, validator

from datamodel import Globals, Record, Table


class ExtendedTable(Table):
    # New fields are declared as class attributes not as instance attributes inside the __init__()
    # Calculated fields usually have a default value or default factory so that you don't have to provide a value
    # I prefer a default_factory for mutable values
    records_by_id: Dict[int, Record] = Field(default_factory=dict)

    # A validator can populate a calculated field
    # Use always=True to run the validator even if a value is not supplied and the default value is used
    @validator("records_by_id", always=True)
    def _calculate_records_by_id(
        cls, value: Dict[int, Record], values: Dict[str, Any]
    ) -> Dict[int, Record]:
        records: Optional[List[Record]] = values.get("records")
        if records is None:
            # The records field was not valid
            # Return value or raise a ValueError instead if you want
            return value
        return {record.id: record for record in records}


class ExtendedGlobals(Globals):
    # You can change the type of a field in a subclass if you declare the field again
    table: ExtendedTable


if __name__ == "__main__":
    records = """
    {
        "table": {
            "records": [{"id": 0, "name": "A"}, {"id": 1, "name": "B"}]
        }
    }
    """

    content = json.loads(records)
    extended_globals = ExtendedGlobals.parse_obj(content)
    print(repr(extended_globals))

Output:

ExtendedGlobals(table=ExtendedTable(records=[Record(id=0, name='A'), Record(id=1, name='B')], records_by_id={0: Record(id=0, name='A'), 1: Record(id=1, name='B')}))
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.