Search code examples
pythonjsonpython-3.xpython-dataclasses

Build a nested data class with lists


I am working on an JSON API-call and I wanted to build it using python data classes. Since I am a beginner the solution may be cumbersome - in that case - please advise a more elegant solution.

So I want my JSON request body to look like this:

{
    "projectFilter": {
        "project": "all",
        "statuses": ["Finished"]
    },
    "currencyId": 0,
    "columns": [
        {"columnType": {
            "kind": "CostActual"
            }
        }
    ]
}

I have modeled this as a nested data class so I can change the API calls in the future. Not sure if this a good way - as I said - beginner here.

import json
from dataclasses import dataclass, field
from typing import List, Dict

@dataclass
class _project_filter():
    project : str = 'all'
    statuses : List = field(default_factory=lambda: ['Finished'])

@dataclass
class _column_type():
    columnType : Dict = field(default_factory=lambda: {'kind' : 'CostActual'})

@dataclass
class project():
    projectFilter: _project_filter = _project_filter()
    currencyId : int = NULL
    columns : List[_column_type] = field(default_factory=_column_type)

    def to_json(self):
        return json.dumps(self.__dict__, default=lambda x: x.__dict__, ensure_ascii=False)

I get the following output when I call the .to_json() method:

{
    "projectFilter": {
        "project": "all",
        "statuses": ["Finished"]
    },
    "currencyId": 0,
    "columns": {
        "columnType": {"kind": "CostActual"}
    }
}

As you can see I don't get an array of the type _column_type. I thought it would work since I have columns : List[_column_type] = field(default_factory=_column_type) Where I though i specified the field columns as a list of _column_type and that I used the default object since I use the default_factory argument.


Solution

  • I would suggest checking out the dataclass-wizard library, as it seems like it'd be a perfect fit for this use case.

    The default case transform used for serialization purposes is camelCase, so the good news is you don't need to pass any additional config to get the desired result when serializing an instance to JSON.


    I started off by piping the sample JSON input to the included CLI utility in order to generate a dataclass schema, and ended up with a dataclass model strucure as below. Note that I went in and added default values for a few fields, similar to how you had it above.

    from __future__ import annotations
    
    from dataclasses import dataclass, field
    
    from dataclass_wizard import JSONWizard
    
    
    @dataclass
    class Project(JSONWizard):
        project_filter: ProjectFilter
        currency_id: int
        columns: list[Column]
    
    
    @dataclass
    class ProjectFilter:
        project: str = 'all'
        statuses: list[str] = field(default_factory=lambda: ['Finished'])
    
    
    @dataclass
    class Column:
        column_type: ColumnType | None = None
    
    
    @dataclass
    class ColumnType:
        kind: str = 'CostActual'
    

    Then I did a quick test to confirm that I'm able to load/dump JSON data using the dataclass schema:

    def main():
        string = """
        {
            "projectFilter": {
                "project": "all"
            },
            "currencyId": 0,
            "columns": [
                {"columnType": {}}
            ]
        }"""
    
        p = Project.from_json(string)
        print(repr(p))
    
        print(p.to_json())
    
        # True
        assert p == p.from_json(p.to_json())
    
    
    if __name__ == '__main__':
        main()
    

    Output:

    Project(project_filter=ProjectFilter(project='all', statuses=['Finished']), currency_id=0, columns=[Column(column_type=ColumnType(kind='CostActual'))])
    {"projectFilter": {"project": "all", "statuses": ["Finished"]}, "currencyId": 0, "columns": [{"columnType": {"kind": "CostActual"}}]}