blog.notmyidea.org/content/code/2024-07-12-pydantic.md
2024-07-12 02:18:10 +02:00

2.6 KiB

title tags
Parsing JSON into Specific Pydantic Models pydantic, python, match

I really like Pydantic because it makes it easy to define the the structure of the objects I want to use, using typing.

One use case I have at the moment is to parse a json object and build different objects depending on some key in the json object.

I tried multiple times, and finally managed to do it:

have different pydantic classes which share a same property (here named kind), and end up with the proper classes at the end.

Here, I have three types of messages: "OperationMessage", "PeerMessage" and "ServerRequest", as follows:

from typing import Literal, Optional
from pydantic import BaseModel


class OperationMessage(BaseModel):
    """Message sent from one peer to all the others"""

    kind: Literal["operation"] = "operation"
    verb: Literal["upsert", "update", "delete"]
    subject: Literal["map", "layer", "feature"]
    metadata: Optional[dict] = None
    key: Optional[str] = None


class PeerMessage(BaseModel):
    """Message sent from a specific peer to a specific one"""

    kind: Literal["peermessage"] = "peermessage"
    sender: str
    recipient: str
    # The message can be whatever the peers want. It's not checked by the server.
    message: dict


class ServerRequest(BaseModel):
    """A request towards the server"""

    kind: Literal["server"] = "server"
    action: Literal["list-peers"]

Each of these classes share the same kind property, which can act as a discriminator.

Let's build a generic Request class that will be able to build for me the proper objects:

from typing import Union
from pydantic import Field, RootModel, ValidationError


class Request(RootModel):
    """Any message coming from the websocket should be one of these, and will be rejected otherwise."""

    root: Union[ServerRequest, PeerMessage, OperationMessage] = Field(
        discriminator="kind"
    )

Which can be used this way:

try:
    incoming = Request.model_validate_json(raw_message)
except ValidationError as e:
    error = f"An error occurred when receiving the following message: {raw_message}"

And, because we have classes, we can leverage the match statement:

match incoming.root:
    case OperationMessage():
        # Here to broadcast the message
        websockets.broadcast(peers, raw_message)

    case PeerMessage(recipient=_id):
        # Or to send peer messages to the proper peer
        peer = connections.get(_id)
        if peer:
            await peer.send(raw_message)