如何限制 python json api 格式

summary

Always use pydantic module

content

API design can be a challenging task, particularly when it comes to deciding on the appropriate output format. One common guide for designing JSON APIs is the simple specification provided by the following resource: https://github.com/omniti-labs/jsend.

However, it can be difficult to ensure that team members consistently adhere to these rules, which can require substantial time investment for reviewing pull requests.

To promote uniformity in API formatting, the most effective strategy may be to incorporate the formatting rules directly into your code. In the following example, I will illustrate how this can be accomplished using Python.

There’s a static type checking module called pydantic which enforces type hints at runtime, and provides user friendly errors when data is invalid.

Below are examples of how this can be done using Python in jsend.

  • response example
from enum import Enum
from typing import Generic, List, TypeVar

from pydantic import BaseModel, Field, validator
from pydantic.generics import GenericModel

T = TypeVar('T')

class ResponseStatusEnum(str, Enum):
    SUCCESS = 'success'
    FAIL = 'fail'
    ERROR = 'error'

class ResponseBaseModel(GenericModel, Generic[T]):
    data: T | None = None
    message: str | None = ''
    status: str = ResponseStatusEnum.success

    class Config:
        use_enum_values = True

sometimes you will need to return data with pagination, here’s the example

  • paginate response example

class PageModel(GenericModel, Generic[T]):
    items: List[T] = []
    total: int
    current_page: int
    last_page: int | None
    prev_page: int | None
    next_page: int | None
    per_page: int

class PaginateResponseBaseModel(GenericModel, Generic[T]):
    data: PageModel[T]
    message: str | None = ''
    status: ResponseStatusEnum = ResponseStatusEnum.SUCCESS

    class Config:
        use_enum_values = True
  • to use it(FastAPI)
from datetime import datetime

class ExampleEntity(BaseModel):
    id: int
    name: str
    age: int
    nick_name: str | None
    created_at: datetime | None = None
    updated_at: datetime | None = None

class GetExampleResponse(ResponseBaseModel[ExampleEntity]):
    data: ExampleEntity

class GetPaginateExamplesResponse(PaginateResponseBaseModel[ExampleEntity]):
    data: PageModel[ExampleEntity]

router = APIRouter()

# {data: {id, name, age, created_at, updated_at}, message, status}
@router.get('/examples/{id}')
def get_example(id) -> GetExampleResponse:
    """
    Returns:
        json: {
            data: { id, name, age, created_at, updated_at },
            message,
            status
        }
    """
    data = repo.get_examples(id)
    return GetExampleResponse(data=data)

@router.get('/paginate-examples/{id}')
def get_examples(id, page:int = 1, per_page: int = 1) -> GetPaginateExamplesResponse:
    """
    Returns:
        json: {
            data: {
                items: [{id, name, age, created_at, updated_at}],
                total,
                current_page,
                last_page,
                prev_page,
                next_page,
                per_page,
            },
            message,
            status
        }
    """
    data = repo.get_examples_with_paginate(id, page, per_page)
    return GetPaginateExamplesResponse(data=data)

Then, all response will be restricted to the formats

references

cmd + /