user6308605
user6308605

Reputation: 731

Pydantic create model for list with nested dictionary

I have a body looks like this:

{
    "data": [
      {
        "my_api": {
          "label": "First name",
          "value": "Micheal"
        }
      },
      {
        "my_api": {
          "label": "Last name",
          "value": [
            "Jackson"
          ]
        }
      },
      {
        "my_api": {
          "label": "Favourite colour",
          "value": "I don't have any"
        }
      }
    ]
  }

This is my model.py so far:

class DictParameter(BaseModel): # pylint: disable=R0903
    """
    `my_api` children
    """
    label: Optional[str]
    value: Optional[str]


class DataParameter(BaseModel): # pylint: disable=R0903
    """
    `data` children
    """
    my_api: Optional[dict]  # NOTE: Future readers, this incorrect reference is part of the OP's Q


class InputParameter(BaseModel): # pylint: disable=R0903
    """
    Takes predefined params
    """
    data: Optional[List[DataParameter]]

In main.py:

from model import InputParameter

@router.post("/v2/workflow", status_code=200)
def get_parameter(user_input: InputParameter):
    """
    Version 2 : No decoding & retrieve workflow params
    """
    data = user_input.data

    print(data)

Output:

[DataParameter(my_api={'label': 'First name', 'value': 'Micheal'}), DataParameter(my_api={'label': 'Last name', 'value': ['Jackson']}), DataParameter(my_api={'label': 'Favourite colour', 'value': "I don't have any"})]

I want to access the value inside my_api key. But I keep getting type error. I'm not sure how to access List of dictionary with nested child. Plus, the value of value can be str or array. It is dynamic.

Is there any other way of doing this?

Upvotes: 3

Views: 4634

Answers (2)

Yaakov Bressler
Yaakov Bressler

Reputation: 12168

Assuming you fix the issue in DictParameter (as pointed out by other answer by @2e0byo):

class DictParameter(BaseModel):
    label: Optional[str]
    value: Optional[Union[str, List[str]]]

And you fix the issue in DataParameter:

class DataParameter(BaseModel):
    # my_api: Optional[dict]  <-- prev value
    my_api: Optional[DictParameter]

You can access the values in your object the following way:

def get_value_from_data_param(param_obj: InputParameter, key: str):
    """
    Returns a value from an InputParameter object,
    or returns `None` if not found
    """

    # Iterate over objects
    for item in param_obj.data:

        # Skip if no value
        if not item.my_api:
            continue
        
        # This assumes there are no duplicate labels
        # if there are, perhaps make a list and append values
        if item.my_api.label == label:
            return item.my_api.value

    # If nothing is found, return None (or some `default`)
    return None

Now let's test it:

input_data = {
    "data": [
        {"my_api": {"label": "First name", "value": "Micheal"}},
        {"my_api": {"label": "Last name", "value": ["Jordan"]}},
        {"my_api": {"label": "Favourite colour", "value": "I don't have any"}}
    ]
}

# Create an object
input_param_obj = InputParameter.parse_obj(input_data)


# Let's see if we can get values:
f_name = get_value_from_data_param(input_param_obj, "First name")
assert f_name == 'Michael'

l_name = get_value_from_data_param(input_param_obj, "Last name")
assert l_name == ['Jordan']

nums = get_value_from_data_param(input_param_obj, "Numbers")
assert nums == ["1", "2", "3"]

erroneous = get_value_from_data_param(input_param_obj, "KEY DOES NOT EXIST")
assert erroneous == None

Upvotes: 0

2e0byo
2e0byo

Reputation: 5964

Plus, the value of value can be str or array. It is dynamic.

What you currently have will cast single element lists to strs, which is probably what you want. If you want lists to stay as lists, use:

from Typing import Union

class DictParameter(BaseModel):
    Value: Union[str, list[str]]

Unless you have the good luck to be running python 3.10, on which case str | list[str] is equivalent.

However, you do not actually use this model! You have my_api: Optional[dict] not my_api: Optional[DictParameter], so your current output is a plain old dict, and you need to do data[0].my_api["value"]. Currently this returns a str or a list, which is probably the problem. I suspect, though, that you meant to use the pydantic schema.

Note that data is a list: if you want all the values you need to iterate, something like

apis = [x.my_api for x in data]

Upvotes: 1

Related Questions