Question
Is it possible to replicate Marshmallow's dump_only
feature using pydantic for FastAPI, so that certain fields are "read-only", without defining separate schemas for serialization and deserialization?
Context
At times, a subset of the attributes (e.g. id
and created_date
) for a given API resource are meant to be read-only and should be ignored from the request payload during deserialization (e.g. when POSTing to a collection or PUTting to an existing resource) but need to be returned with that schema in the response body for those same requests.
Marshmallow provides a convenient dump_only
parameter that requires only one schema to be defined for both serialization and deserialization, with the option to exclude certain fields from either operation.
Existing Solution
Most attempts I've seen to replicate this functionality within FastAPI (i.e. FastAPI docs, GitHub Issue, Related SO Question) tend to define separate schemas for input (deserialization) and output (serialization) and define a common base schema for the shared fields between the two.
Based on my current understanding of this approach, it seems a tad inconvenient for a few reasons:
- It requires the API developer to reserve separate namespaces for each schema, a problem that is exacerbated by following the practice of abstracting the common fields to a third "base" schema class.
- It results in the proliferation of schema classes in APIs that have nested resources, since each level of nesting requires a separate input and output schema.
- The the OAS-compliant documentation displays the input/output schemas as separate definitions, when the consumer of that API only ever needs to be aware of a single schema since the (de)serialization of those read-only fields should be handled properly by the API.
Example
Say we're developing a simple API for a survey with the following models:
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy import (
func,
Column,
Integer,
String,
DateTime,
ForeignKey,
)
Base = declarative_base()
class SurveyModel(db.Base):
"""Table that represents a collection of questions"""
__tablename__ = "survey"
# columns
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
created_date = Column(DateTime, default=func.now())
# relationships
questions = relationship("Question", backref="survey")
class QuestionModel(Base):
"""Table that contains the questions that comprise a given survey"""
__tablename__ = "question"
# columns
id = Column(Integer, primary_key=True, index=True)
survey_id = Column(Integer, ForeignKey("survey.id"))
text = Column(String)
created_date = Column(DateTime, default=func.now())
And we wanted a POST /surveys
endpoint to accept the following payload in the request body:
{
"name": "First Survey",
"questions": [
{"text": "Question 1"},
{"text": "Question 2"}
]
}
And return the following in the response body:
{
"id": 1,
"name": "First Survey",
"created_date": "2021-12-12T00:00:30",
"questions": [
{
"id": 1,
"text": "Question 1",
"created_date": "2021-12-12T00:00:30"
},
{
"id": 2,
"text": "Question 2",
"created_date": "2021-12-12T00:00:30"
},
]
}
Is there an alternative way to make id
and created_date
read-only for both QuestionModel
and SurveyModel
other than defining the schemas like this?
from datetime import datetime
from typing import List
from pydantic import BaseModel
class QuestionIn(BaseModel):
text: str
class Config:
extra = "ignore" # ignores extra fields passed to schema
class QuestionOut(QuestionIn):
id: int
created_date: datetime
class SurveyBase(BaseModel):
name: str
class Config:
extra = "ignore" # ignores extra fields passed to schema
class SurveyOut(SurveyBase):
id: int
created_date: datetime
class SurveyQuestionsIn(SurveyBase):
questions: List[QuestionIn]
class SurveyQuestionsOut(SurveyOut):
questions: List[QuestionOut]
Just for comparison, here would be the equivalent schema using marshmallow:
from marshmallow import Schema, fields
class Question(Schema):
id = fields.Integer(dump_only=True)
created_date = fields.DateTime(dump_only=True)
text = fields.String(required=True)
class Survey(Schema):
id = fields.Integer(dump_only=True)
created_date = fields.DateTime(dump_only=True)
name = fields.String(required=True)
questions = fields.List(fields.Nested(Question))