Introduction: when the built-in fields aren't enough
fields.Str(), fields.Int(), fields.Email() — marshmallow's built-in fields are powerful, and the majority of an API's input/output can be covered with just these. But a real-world domain inevitably has values with business rules that the built-ins can't fully express.
- Amount: hold the currency as
Decimal, round to 2 decimal places, and never handle it asfloat. - Phone number: a user inputs both
090-1234-5678and09012345678, but internally you want to always normalize to the E.164 form+819012345678. - Postal codes, SKU codes, internal IDs — values where "the format is fixed, accompanied by validation and normalization."
"Validating such a value with an if statement each time and shaping it with .replace()" in a view function or the service layer is a typical anti-pattern. The same normalization logic is copied per API, and in one place +81 is attached but in another it's forgotten — the knowledge scatters and will definitely clash someday.
The idea of a value object in domain-driven design pays off here. "Amount" and "phone number" aren't mere strings but types with inherent constraints. Apply those constraints once at the system boundary, and flow only normalized values inside. marshmallow's custom fields are exactly the mechanism for expressing this "one-time normalization / validation at the boundary" as a reusable part.
💡 This article is a sequel to the marshmallow practical guide (v4-compatible), which explained marshmallow's basics —
Schema/load()/dump()/ three-layer validation / the security boundary. It proceeds on the premise thatload()is the entrance to boundary validation and the responsibility separation of@validates/@validates_schema. If you haven't read it, please go there first.
💡 The version covered in this article: it assumes marshmallow 4.3.0 (the stable version as of April 2026). As an important v4 change,
fields.Fieldwas genericized, letting you declare with a type argument,class MyField(fields.Field[T]), "the Python typeTthis field returns after deserialization." This isn't mere decoration; it has the practical benefit of raising the accuracy of IDE completion and static analysis.
1. Anatomy of a custom field: _serialize and _deserialize
The implementation of a custom field, boiled down, comes down to writing just 2 methods. Inherit fields.Field[T] and implement the output-direction _serialize and the input-direction _deserialize.
from marshmallow import ValidationError, fields
class PinCode(fields.Field[list[int]]):
"""暗証番号を「内部では int のリスト、外向きには文字列」として双方向変換するフィールド。"""
def _serialize(self, value, attr, obj, **kwargs):
# dump 方向:内部の [1, 2, 3, 4] を "1234" という文字列にする
if value is None:
return ""
return "".join(str(d) for d in value)
def _deserialize(self, value, attr, data, **kwargs):
# load 方向:外部の "1234" を内部の [1, 2, 3, 4] に変換し、不正なら弾く
try:
return [int(c) for c in value]
except ValueError as error:
raise ValidationError("Pin codes must contain only digits.") from error
The roles and arguments of the 2 methods are fixed by the official spec as follows.
| Method | Direction | The value it receives | What it should return |
|---|---|---|---|
_serialize(self, value, attr, obj, **kwargs) | dump (output) | The internal Python value | A JSON-compatible outward representation |
_deserialize(self, value, attr, data, **kwargs) | load (input) | The external raw data | A validated, normalized internal value T |
_serialize'sobjis the whole source object, and_deserialize'sdatais the whole input dict. You can use them when you want to reference another field's value, but the basic rule is to complete it with just the 1st argumentvalue.- The value
_deserializereturns goes intoload()'s result dict. ForPinCode,load({"pin": "1234"})returns{"pin": [1, 2, 3, 4]}, anddump({"pin": [1, 2, 3, 4]})returns{"pin": "1234"}— the key is that the round-trip is closed.
from marshmallow import Schema
class LoginSchema(Schema):
pin = PinCode()
LoginSchema().load({"pin": "1234"}) # → {'pin': [1, 2, 3, 4]}
LoginSchema().dump({"pin": [1, 2, 3, 4]}) # → {'pin': '1234'}
LoginSchema().load({"pin": "12ab"}) # → ValidationError: Pin codes must contain only digits.
Why is this superior?
The knowledge of "convert the string "1234" into a list of int, and reject anything non-numeric" coheres in one type called PinCode. Every schema using this type automatically gets the same conversion and validation — a typical practice of DRY. Furthermore, thanks to the type argument fields.Field[list[int]], the contract "this field's _deserialize returns list[int]" is made explicit as a type, paying off for ETC (Easy To Change) too. Even if the conversion rule changes, the fix is only inside PinCode.
2. Error design and i18n: manage messages centrally with make_error
In PinCode I hard-wrote the error message inside _deserialize. It works, but because the message is embedded in the implementation logic, multilingual support and unifying the wording are difficult. marshmallow solves this problem with default_error_messages and make_error.
from marshmallow import ValidationError, fields
class PinCode(fields.Field[list[int]]):
# フィールドクラスにエラーメッセージを「キー → 文言」の辞書として宣言する
default_error_messages = {
"invalid": "暗証番号は数字のみで構成してください。",
"length": "暗証番号は4桁である必要があります。",
}
def _serialize(self, value, attr, obj, **kwargs):
if value is None:
return ""
return "".join(str(d) for d in value)
def _deserialize(self, value, attr, data, **kwargs):
try:
digits = [int(c) for c in value]
except (TypeError, ValueError) as error:
# メッセージ本文ではなく「キー」で参照する
raise self.make_error("invalid") from error
if len(digits) != 4:
raise self.make_error("length")
return digits
self.make_error("invalid") pulls the wording corresponding to the key "invalid" from default_error_messages, constructs a ValidationError, and returns it. You could also write raise ValidationError("...") directly, but make_error has the next advantages.
- Central management of messages: all the wording coheres in the
default_error_messagesdict, separated from the implementation logic. - Per-instance override: if the call site passes
error_messages={...}at field creation, it can replace only the wording of the same key. It's the same mechanism as translating a built-in field'srequiredmessage into Japanese.
from marshmallow import Schema, fields
class CheckoutSchema(Schema):
# 組み込みフィールドも同じ仕組み。キー単位でメッセージを上書きできる
email = fields.Email(
required=True,
error_messages={"required": "メールアドレスは必須です。", "invalid": "メール形式が不正です。"},
)
# 自作フィールドの "length" だけ、この画面用に差し替える
pin = PinCode(error_messages={"length": "4桁の暗証番号を入力してください。"})
Why is this superior?
make_error("invalid") references the meaning (the key) of the wording, not the wording's substance. With this, the validation logic of "something non-numeric came" and the display concern of "what to tell the user" are separated (SRP). The requirement to insert i18n resources, or to change the wording per screen — that can be realized with just replacing error_messages, without touching the contents of _deserialize at all. This is the dividend of "keeping logic and messages separate."
⚠️ Don't swallow exceptions in
_deserialize:[int(c) for c in value]throws aTypeErrorifvalueisNoneorint, and aValueErrorif it contains non-digits. If you catch these withtry/except, always convert them to aValidationError(ormake_error) and re-send. Let a rawTypeErrorleak outside as-is, and it slips past marshmallow's error-aggregation mechanism (structuring intoerr.messages, indexing withmany=True) and is exposed to the user as a 500 error. Keep the original exception in__cause__withraise ... from error, and you don't lose the clue for log investigation either.
3. Real example ①: an amount field (Decimal-based)
The domain type with the highest "value of building yourself" is the amount. Handling an amount as float is a dangerous choice directly connected to production incidents.
⚠️ Don't handle an amount as
float: it's famous that0.1 + 0.2becomes0.30000000000000004, which happens because IEEE 754 floating point can't accurately represent decimal fractions in binary. Let this accumulate in amount calculations and it becomes an accounting error that must not happen — the billed amount being off by 1 yen, the total not matching. Always hold an amount asdecimal.Decimal, and output it to JSON as a string (output asfloatand the error mixes in again on the JSON-parser side).
marshmallow has fields.Decimal, with places (decimal digits), as_string (string output), and rounding (rounding mode). Subclass this to make a MoneyField with the currency domain's convention baked in as the default.
import decimal
from marshmallow import ValidationError, fields, validate
class MoneyField(fields.Decimal):
"""通貨額を表すフィールド。小数第2位・四捨五入(ROUND_HALF_UP)・文字列出力をデフォルトに固定する。"""
default_error_messages = {
"negative": "金額は0以上である必要があります。",
}
def __init__(self, *, allow_negative: bool = False, **kwargs):
self.allow_negative = allow_negative
# ドメイン規約をデフォルト値として設定する。呼び出し側が明示すれば上書き可能
kwargs.setdefault("places", 2)
kwargs.setdefault("rounding", decimal.ROUND_HALF_UP)
kwargs.setdefault("as_string", True) # JSON には文字列で出す(精度保持)
super().__init__(**kwargs)
def _deserialize(self, value, attr, data, **kwargs):
# まず親(fields.Decimal)に Decimal への変換・桁丸めを任せる
amount = super()._deserialize(value, attr, data, **kwargs)
if not self.allow_negative and amount < 0:
raise self.make_error("negative")
return amount
Usage is no different from a built-in field.
from marshmallow import Schema
class OrderSchema(Schema):
subtotal = MoneyField(required=True)
# 値引きはここでは正負の検証だけ。Range など追加ルールは validate= で合成できる
discount = MoneyField(allow_negative=False, validate=validate.Range(min=0))
OrderSchema().load({"subtotal": "1980.005", "discount": "200"})
# → {'subtotal': Decimal('1980.01'), 'discount': Decimal('200.00')} ← 桁丸め済み
OrderSchema().dump({"subtotal": decimal.Decimal("1980.01")})
# → {'subtotal': '1980.01', ...} ← float ではなく文字列で出力
The point is that in _deserialize, you first delegate the conversion to the parent (super()._deserialize(...)), then add domain-specific validation (the negative check) to the result. Without reinventing the "wheel" of conversion to Decimal and digit rounding, you only add the business rule on top.
Why is this superior?
The currency-domain convention of "amounts are 2 decimal places, round, string output, no negatives" is confined in one value object called MoneyField. Declare all amount fields in the project with MoneyField, and the fraying of the convention — outputting float in only one API, forgetting rounding in only one screen — becomes structurally impossible. When I designed billing / payments in the lumber-distribution B2B SaaS too, unifying the handling of amounts at the type level was the foundation that ensured billing accuracy.
4. Real example ②: a phone-number field (E.164 normalization)
A phone number is a typical example where "the user's input format is diverse" and "internally you want to unify to one normal form." 090-1234-5678, 09012345678, +81 90 1234 5678 — make a field that normalizes all of these to the E.164 form +819012345678 and rejects invalid input.
import re
from marshmallow import ValidationError, fields
# 国内番号(0始まり、10〜11桁)を想定したシンプルな正規化例
_DOMESTIC_RE = re.compile(r"^0\d{9,10}$")
class PhoneField(fields.Field[str]):
"""日本国内の電話番号を E.164 形式(+81...)に正規化するフィールド。"""
default_error_messages = {
"invalid": "電話番号の形式が正しくありません。",
}
def _serialize(self, value, attr, obj, **kwargs):
# 内部では常に +81... の正規形なので、そのまま出力する
return value if value is not None else None
def _deserialize(self, value, attr, data, **kwargs):
if not isinstance(value, str):
raise self.make_error("invalid")
# 数字以外(ハイフン・空白・括弧)を除去してから判定する
digits = re.sub(r"[^\d]", "", value)
if _DOMESTIC_RE.match(digits):
# 先頭の 0 を国番号 +81 に置き換える(E.164 化)
return "+81" + digits[1:]
# 既に +81 始まりの国際形式で来たケースも受理する
if value.startswith("+81") and re.fullmatch(r"\+81\d{9,10}", value):
return value
raise self.make_error("invalid")
from marshmallow import Schema
class ContactSchema(Schema):
phone = PhoneField(required=True)
ContactSchema().load({"phone": "090-1234-5678"}) # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "09012345678"}) # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "+819012345678"}) # → {'phone': '+819012345678'}
ContactSchema().load({"phone": "1234"}) # → ValidationError: 電話番号の形式が正しくありません。
💡 The above is a minimal implementation to show the design pattern of normalization. To strictly handle international numbers, landlines, and regional digit-count differences in real operation, a configuration of calling
phonenumbers(a Python port of Google libphonenumber) inside_deserializeand returning the result ofphonenumbers.format_number(..., E164)is robust. The point is that the structure of "confining the normalization and validation logic inside_deserialize" is the essence, and the internal algorithm is swappable.
Why is this superior?
The responsibility of absorbing the wobble of input formats (with/without hyphens, international notation) and aligning to the single normal form coheres in the single PhoneField. Use it for all APIs' phone-number input, and the invariant that the phone number saved in the DB is always E.164 is guaranteed at the boundary. Downstream processing — SMS sending, duplicate checks, deduplication — can trust the premise that "phone numbers are normalized," and each process no longer needs to re-implement normalization individually. This is the value-object philosophy of "normalize once at the boundary" itself.
5. Real example ③: enums (fields.Enum) — safely handle Python's Enum at the boundary
A "one of the predetermined options" like a status or a kind is type-safe to express with Python's standard enum.Enum. marshmallow has a dedicated fields.Enum, and using it lets you safely convert a string from outside into an Enum member at the boundary. fields.Enum needs no self-building — the built-in is enough.
import enum
from marshmallow import Schema, fields
class OrderStatus(enum.Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
CANCELLED = "cancelled"
class OrderSchema(Schema):
# デフォルト:メンバーの「名前」でシリアライズ/デシリアライズする
status_by_name = fields.Enum(OrderStatus)
# by_value=True:メンバーの「値」でシリアライズ/デシリアライズする
status_by_value = fields.Enum(OrderStatus, by_value=True)
The difference between the 2 modes is whether the outward representation uses the name or the value.
| Declaration | Input load accepts | dump's output |
|---|---|---|
fields.Enum(OrderStatus) | "PAID" (the member name) | "PAID" |
fields.Enum(OrderStatus, by_value=True) | "paid" (the member value) | "paid" |
schema = OrderSchema()
schema.load({"status_by_name": "PAID", "status_by_value": "paid"})
# → {'status_by_name': <OrderStatus.PAID: 'paid'>, 'status_by_value': <OrderStatus.PAID: 'paid'>}
# 列挙にない値は ValidationError で弾かれる
schema.load({"status_by_name": "UNKNOWN", "status_by_value": "paid"})
# → ValidationError: Must be one of: PENDING, PAID, SHIPPED, CANCELLED.
Which to choose is decided by the external API's contract. If the front end or external system exchanges "paid" (the lowercase value), by_value=True; if "PAID" (the member name), leave it default.
Why is this superior?
Write the enum's option check as a string list with validate.OneOf(["pending", "paid", ...]), and the list and the actual Enum definition become double-managed, inviting a DRY violation of forgetting to change one. fields.Enum(OrderStatus) makes the Enum definition itself the single truth, and at the point it passes load(), the value is not a raw string but a typed Enum member. Downstream code can branch safely with an is comparison like if order.status is OrderStatus.PAID:, and a missed branch due to a string typo is prevented at the type level.
6. Computed / derived values: fields.Method / Function / Constant
Not every field corresponds to input. There are scenes where you want to declare "a value computed from other fields" or "a value the server returns fixed" as output-only. marshmallow provides 3 lightweight mechanisms.
fields.Function: compute with an on-the-spot lambda
from marshmallow import Schema, fields
class UserSchema(Schema):
first_name = fields.Str()
last_name = fields.Str()
# obj(変換元オブジェクト全体)を受け取り、その場で算出する
upper_name = fields.Function(lambda obj: obj["first_name"].upper())
fields.Function takes a callable for serialize, and can also specify the reverse direction with deserialize= if needed. It suits a simple computation that completes in one line.
fields.Method: entrust it to a schema method
If the computation logic is complex, or you want to share it across multiple fields, fields.Method pointing at a schema method is more readable.
from marshmallow import Schema, fields
class OrderSchema(Schema):
subtotal = MoneyField(required=True)
tax_rate = fields.Decimal(load_default="0.10")
# serialize 時は total_amount を、deserialize 時は parse_total を呼ぶ
total = fields.Method("total_amount", deserialize="parse_total")
def total_amount(self, obj):
# 小計 × (1 + 税率) を算出して返す
return str(obj["subtotal"] * (1 + obj["tax_rate"]))
def parse_total(self, value):
# 入力側で受け取りたい場合の変換(不要なら deserialize= を省略すれば dump 専用になる)
return decimal.Decimal(value)
fields.Method's serialize method takes the signature def method(self, obj): ..., and the deserialize method def method(self, value): ....
fields.Constant: always output the same value
To return a fixed value regardless of input — like an API version tag or a response kind — use fields.Constant.
from marshmallow import Schema, fields
class ResponseSchema(Schema):
api_version = fields.Constant("2026-06") # 常にこの値を出力する
object_type = fields.Constant("order")
Combination with dump_only
Computed / derived values are essentially output-only in most cases. Letting the client write total becomes room for tampering, so use dump_only=True together to block input.
class OrderSchema(Schema):
subtotal = MoneyField(required=True)
# 算出値はサーバーが決める。load では受け付けない
total = fields.Function(lambda obj: str(obj["subtotal"] * decimal.Decimal("1.10")), dump_only=True)
Why is this superior?
Declare logic like "computing the total amount" or "attaching a version tag" as fields.Method / Function / Constant in the schema definition, and the response-shaping knowledge coheres in the schema. You no longer need to assemble it by hand inside the view function, like response["total"] = subtotal * 1.1, preventing business logic from leaking into the presentation layer (SRP). Even if the output spec changes, the fix completes inside the schema.
7. Extending an existing field: subclassing and _bind_to_schema
Inheriting fields.Field[T] from scratch isn't the only hand. Subclassing an existing field and overriding _deserialize to always apply common preprocessing is an extremely practical pattern.
The representative example is a field that "always trims (removes leading/trailing whitespace from) a string." Rather than writing it per schema with @pre_load, giving it as a type is more reusable.
from marshmallow import fields
class TrimmedString(fields.String):
"""前後の空白を常に除去する文字列フィールド。空白起因のバグを型レベルで防ぐ。"""
def __init__(self, *, lower: bool = False, **kwargs):
self.lower = lower
super().__init__(**kwargs)
def _deserialize(self, value, attr, data, **kwargs):
# まず親(fields.String)に文字列としての検証を任せる
result = super()._deserialize(value, attr, data, **kwargs)
result = result.strip()
if self.lower:
result = result.lower()
return result
from marshmallow import Schema, validate
class SignupSchema(Schema):
name = TrimmedString(required=True, validate=validate.Length(min=1))
# メールは小文字化+トリムを型に焼き込む。validate= の検証はトリム後の値にかかる
email = TrimmedString(lower=True, validate=validate.Email())
SignupSchema().load({"name": " 友田 ", "email": " Tomoda@Example.com "})
# → {'name': '友田', 'email': 'tomoda@example.com'}
Note that with super()._deserialize(...) you reuse the parent's "validation as a string type" while adding only the normalization of strip() / lower(). Without reinventing the wheel, adding only the behavior — this is the royal road of extension.
_bind_to_schema: a hook at schema binding
When a field needs to know "the schema it belongs to" — for example, an advanced case of switching the error message according to the parent schema's setting, or resolving a reference to another field — you touch the _bind_to_schema hook. It's called the moment the field is bound to the schema.
from marshmallow import fields
class ContextAwareField(fields.String):
def _bind_to_schema(self, field_name, parent):
# v4 ではフックの第2引数が `schema` から `parent` に改名された点に注意
super()._bind_to_schema(field_name, parent)
# ここで self.parent(属するスキーマ)や field_name にアクセスできる
# 例:親スキーマの状態に応じてこのフィールドの挙動を初期化する
💡
_bind_to_schema's argument name changed fromschematoparentin marshmallow 4 (because a field can be bound not only to a schema but also inside another field, it became a more accurate name). When porting 3.x code, match the signature. You don't touch it day-to-day much, but it's useful in advanced extensions where you want to give a field schema context.
Why is this superior?
TrimmedString bakes into the type the invariant that "the input string is always trimmed." Compared to the method of writing normalization per schema with @pre_load, because normalization is guaranteed at the point the field is declared, forgetting to write it can't happen. This way of assembling a domain type with composition (an existing field + added behavior) is the embodiment of "composition over inheritance" and "ETC" as in CLAUDE.md.
8. Composing validators: validate.And and reusable functions
Whereas a custom field handles "type conversion and normalization," the validate= argument declares "the constraint the value should satisfy." The two are complementary, and you can compose multiple validators on one field.
from marshmallow import Schema, fields, validate
class ProductSchema(Schema):
# リストで渡すと、すべてのバリデータが順に適用される
sku = fields.Str(validate=[validate.Length(equal=8), validate.Regexp(r"^[A-Z0-9]+$")])
# validate.And は同じ合成を1つのバリデータオブジェクトとして表現する(再利用しやすい)
code = fields.Str(validate=validate.And(validate.Length(equal=8), validate.Regexp(r"^[A-Z0-9]+$")))
validate=[a, b] (a list) and validate.And(a, b) (a composition object) have the same effect, but validate.And(...) is superior in being able to bind the composed validator to a variable and reuse it. Define project-common constraints once, with a name.
from marshmallow import validate
# プロジェクト共通の SKU 制約を「ひとつの再利用可能なバリデータ」として定義する
sku_validator = validate.And(
validate.Length(equal=8),
validate.Regexp(r"^[A-Z0-9]+$", error="SKUは英大文字と数字のみ8桁です。"),
)
class ProductSchema(Schema):
sku = fields.Str(validate=sku_validator) # 使い回す
class InventorySchema(Schema):
sku = fields.Str(validate=sku_validator) # 別スキーマでも同じ制約
A self-made validator function: raise a ValidationError
A constraint that can't be expressed with built-in validators is written as a function that sends a ValidationError.
from marshmallow import ValidationError
def validate_even(value: int) -> None:
# v4 ではバリデータは False を返すのではなく ValidationError を raise する
if value % 2 != 0:
raise ValidationError("偶数を指定してください。")
class LotSchema(Schema):
quantity = fields.Int(validate=[validate.Range(min=0), validate_even])
⚠️ A v4 validator must not return
False: in marshmallow 3.x you could use a validator returning abool, likevalidate=lambda x: x > 0, but in v4 you must alwaysraiseaValidationErroron validation failure.return Falseis ignored, and the validation slips through. The code generative AI outputs and old articles are often in the 3.x style, so beware when porting.
Distinguishing validate= and @validates
| Means | Suited scene | Context other than the value |
|---|---|---|
validate= (a validator / composition) | A static rule that completes with the value alone (length, range, regex). Easy to reuse | Can't be used (sees only the value) |
@validates("field") | Field validation needing to reference the schema instance or other state | Can access the schema's self |
@validates_schema | An invariant spanning multiple fields (start < end, etc.) | Can reference the whole input dict |
The principle is "make value-alone static rules reusable with validate=, and if context is needed, with the @validates family." Write in @validates something you could lean to validate=, and reuse doesn't work, and the same logic duplicates per schema.
Why is this superior?
Name and compose a constraint like sku_validator, and that constraint becomes one reusable part. When the SKU's digit-count spec changes, the fix completes in one place, the definition of sku_validator, and it's automatically reflected to all schemas using it. Keeping "the single truth of the validation rule" — this is a design extending DRY all the way to the validation domain.
Conclusion: design domain types as "boundary parts"
When the built-in fields aren't enough, that shortfall is a sign of "design a domain-specific value object." marshmallow's custom fields are a proven mechanism for expressing that value object as a reusable part at the boundary. Let me re-list this article's key points.
- Inherit
fields.Field[T]and implement_serialize/_deserialize, and you can confine domain-specific type conversion and validation in one place and reuse it across all schemas. - Invalid input is converted to and sent as a
ValidationErrorinside_deserialize, not leaking a raw exception. Manage wording centrally withdefault_error_messages+make_error, and leave it open to i18n / per-screen replacement witherror_messages. - Make values with business rules into types: amounts as a
MoneyFieldwithDecimal, string output, and rounding baked in, and phone numbers as aPhoneFieldnormalizing to E.164. Don't handle amounts asfloat. - Enums as
fields.Enum(value-based withby_value=True), passing the boundary as a typed member rather than a raw string. - Declare computed values with
fields.Method/Function, and fixed values withfields.Constant, and usedump_onlytogether to prevent the scattering of business logic. - Subclass existing fields (
TrimmedString) and compose withvalidate.Andto assemble validation logic DRY-ly. A validator must alwaysraiseaValidationErrorin v4.
The difference between "working code" and "code you can operate for 10 years" lies in where and how you normalize / validate domain values, and how much you can localize that knowledge. A custom field elevates one-time normalization at the boundary into a reusable type, making the fraying of the convention structurally impossible.
For further exploration, I recommend re-reading the following of the official documentation with this article's design viewpoint in mind.
- Custom Fields
- API Reference: fields
- API Reference: validate
- Upgrading to newer releases (3→4 migration)
Consultation on type-safe backend design
The author has implemented and operated the discipline explained here — "normalize / validate domain values once at the boundary, and flow only typed, safe values inside" — in the production environment of a B2B SaaS that won the Minister of Economy, Trade and Industry Award. Designing values with business rules — amounts, phone numbers, various codes — as reusable custom fields directly connects to billing accuracy, data consistency, and maintainability. I build, fast and high-quality with generative AI, the foundation directly connected to a business's reliability — type-safe input validation, domain-type design, response shaping, and ORM integration. On backend development using Python and the type-safe-ification of existing systems, feel free to consult me.