Search code examples
pythonjsonpython-3.xpropertiesgetter-setter

Create class using getter-setter which gets data from json using python3


I'm building an application using REST APIs for a 3rd party application. When I retrieve an object from the API it sends me a json which I pass into a class that internally takes out keys and provides the data via the class using property

Example:

Payload:

{
  "value":{
    "boot":{
      "delay":10,
      "legacy_boot":false
    }
  }
}
class AbstractInfo:
  def __init__(self, json):
    try:
      self.info = json["value"]
    except KeyError:
      # KeyError is raised when nested dictionary is passed which does not contain `value` key
      self.info = json

  def to_dict(self):
    return self.info

class BootInfo(AbstractInfo):

  @property
  def delay(self):
    return self.info["delay"]

  @property
  def legacy_boot(self):
    return self.info["legacy_boot"]

class MainInfo(AbstractInfo):
  @property
  def boot(self):
    return BootInfo(json=self.info["boot"])

Now, from some other process, I call the MainInfo class using dynamic imports and do something like this

# Do rest API call and get the payload in a variable
MainInfo(json=payload)

This all works well till the time, I'm only trying to fetch the values from the payload like so

# GetInfo does dynamic import for the correct class based on the resource_id
info = GetInfo("resource_id")
print(info.boot.delay)

Now, my objective is to create an object using the schema defined in BootInfo so that it returns the relevant dictionary.

Here are results of print statements to help clear things up

print(MainInfo(json=json).to_dict())

{'boot': {'delay': 10, 'legacy_boot': False}}

print(MainInfo(json=json).boot.to_dict())

{'delay': 10, 'legacy_boot': False}

I would like to do something like

bi = BootInfo(delay=20, legacy_boot=True).build_dict()
print(bi)
{'delay': 20, 'legacy_boot': True}
# Then use `bi` into another place to mount the dictionary easily like building a lego set

I'm aware that my BootInfo does not accept delay and legacy_boot as the argument. If I add them, then how do I set the values when the data arrives via json? I feel like I'm in a chicken and egg situation here. And, if possible, I would like to keep the current structure of the code as is so that it's less work to find the keys from the nested structure. The more nested structure I have, the more classes I'm making. I'm also doing validation under properties when a certain key is dependent on the value from another property and might not be available otherwise in the json payload.

I have looked into getter and setter and tried a few things and had several issues and just could not make my requirements work.

Does anyone know the best way to get this done or show me the right direction with a sample code?


Solution

  • I think this is a prime example on when to use a validator library. (personal favorite: https://pydantic-docs.helpmanual.io/)

    Advantages:

    1. you can validate the data that you are fetching over the API by strongly type the expected json and nested structures.
    2. It has all the helpers methods to convert dict to class and to convert class to dict, as well as methods for reading up json strings if necessary.

    Example:

    from pydantic import BaseModel
    
    class BootInfo(BaseModel):
      delay: int
      legacy_boot: bool
    
    class MainInfo(BaseModel):
      boot: BootInfo
    
    
    class Info(BaseModel):
      value: MainInfo
    
    

    then you can do:

    payload = {
      "value":{
        "boot":{
          "delay":10,
          "legacy_boot":false
        }
      }
    }
    
    info = Info(**data)
    info.value.boot.delay
    info.value.boot.legacy_boot
    
    assert info.dict() == payload
    
    # or init the class by properties
    boot_info = BootInfo(delay=10, legacy_boot=False)
    assert boot_info.delay == 10
    boot_info.dict()