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))
Config, though.ignoreis the default value forextra.Config. I'm looking for a solution that doesn't require defining separate schema classes for inputs and outputs for the reasons listed under "Existing Solution". This solution isn't too complex for the toy example above but it requires 2-3x the number of classes as Marshmallow's approach withdump_onlywhich becomes less manageable as the number of models increases.#3link? Also, your use of the termread-onlyis confusing as it makes me think you are trying to prevent modification of the field.idis one option for setting the id value, but it still requires a separate pydantic class to return theidin the response body once it's been set. And that is actually what I mean byread-only. It is a common pattern within Marshmallow that they calldump_onlywhich allows a value to be serialized but not deserialized: marshmallow.readthedocs.io/en/stable/…