BioGeek
BioGeek

Reputation: 22847

Dynamically create Python object from JSON returned by REST API

Say I have two REST API endpoints (http://example.com/task, http://example.com/employee) and the following two classes:

class Employee(object):
    def __init__(self, **kwargs):
        self.ID = kwargs['ID']
        self.first_name = kwargs['first_name'] if 'first_name' in kwargs else None
        self.last_name = kwargs['last_name'] if 'last_name' in kwargs else None

class Task(object):
    def __init__(self, **kwargs):
        self.ID = kwargs['ID']
        self.start_date = kwargs['start_date']
        self.employee = Employee(ID=kwargs['employee_id'])

I now want to create Python objects from the json returned by the REST API.

Say that a GET to http://example.com/task/19 returns the following json:

json_data =  {'ID': '19', 'start_date': '2018-04-23', 'employee_id': 'xyz1223'}
task = Task(**json_data)

Now, the following all work

print(task.ID) # 19
print(task.start_date) # 2018-04-23
print(task.employee.ID) # xyz1223

but print(task.employee.first_name) returns None. What I would like to happen is that under the hood a GET request to http://example.com/employee/xyz1223 is sent, that the resulting json is parsed, and that the attributes first_name and last_name are filled in.

What is the most pythonic way of doing this?

Upvotes: 1

Views: 1738

Answers (1)

chepner
chepner

Reputation: 531265

I would keep the __init__ methods as simple and as stupid as possible, which means no I/O. That work is deferred to a class method named from_api, which just takes the requested ID as an argument, then builds the appropriate URI, parses the output, and passes the necessary data to the call to __init__.

Employee.from_api is still pretty simple, but Task.from_api is where you implement the "join" by calling Employee.from_api with an argument taken from the parsed task JSON data.

class Employee(object):
    BASE_URL = "http://example.com/employee/"

    def __init__(self, id_, first, last):
        self.id_ = id_
        self.first_name = first
        self.last_name = last

    @classmethod
    def from_api(cls, id_):
        url = cls.BASE_URL + id_
        json_data = requests.get(url).json()
        return Employee(id_=id_
                        first=json_data.get('first_name'),
                        last=json_data.get('last_name'))


class Task(object):
    BASE_URL = "http://example.com/task/"

    def __init__(self, id_, start_date, emp):
        self.ID = id_
        self.start_date = start_date
        self.employee = emp

    @classmethod
    def from_api(cls, id_):
        url = cls.BASE_URL + id_
        json_data = requests.get(url).json()
        emp = Employee.from_api(json_data['employee_id'])
        return Task(id_, json_data['start_date'], emp)


task = Task.from_api("19")

Such a design is well-suited to Python 3.7's forthcoming data classes

@dataclass
class Employee:
    id_: str
    first_name: str
    last_name: str

    BASE_URL: ClassVar[str] = "http://example.com/employee/"


    @classmethod
    def from_api(self, id_):
        # same as above


@dataclass
class Task:
    id_: str
    start_data: str  # Or maybe datetime.date, depending on your design
    employee: Employee

    BASE_URL: ClassVar[str] = "http://example.com/task/"

    @classmethod
    def from_api(self, id_):
        # same as above

Upvotes: 2

Related Questions