ElasticModel is a wrapper around pydantic.BaseModel(v2) designed to simplify working with partial (projected) data from databases or APIs.
Key advantage: nested models (“model-in-model”) work even when not all required fields are loaded. Methods remain usable at any level and operate on the data that is loaded.
Pydantic’s
model_validate()requires all required fields.model_construct()allows partials but keeps nested dicts as raw dicts. ElasticModel combines the best of both.
Core features of ElasticModel:
- ✅ Fully inherits the behavior of
pydantic.BaseModel: all methods and functionality. Just replaceBaseModelwithElasticModel. - ✅ Allows creating models from incomplete data, providing full access — read fields, call methods, even on nested models.
- ✅ Avoids the need for declaring a bunch of
Optionalfields. - ✅ Supports dynamic unknown fields — stored in
self.elastic_extra, a dictionary for unprocessed data. - ✅ Ensures strict access: only loaded fields can be accessed. Trying to access an unloaded field will raise
NotLoadedFieldError. - ✅ Supports recursive creation of nested models.
- ✅ Provides "shallow" or "deep" checks for required fields at any time (on demand).
ElasticModel combines the best of BaseModel.model_validate (structured/nested models) and BaseModel.model_construct (creation without validation), adding new flexibility for working with partial data — without overloading your codebase with Optional fields.
🌐Переклад тут🔱 ElasticModel — це `pydantic.BaseModel`(v2), який надає ... та спрощує...
... спрощує роботу з частковими (проекційними) даними з баз даних або API.Основна перевага — підтримка вкладених моделей ("модель у моделі") без необхідності завантаження всіх полів. Незважаючи на це, методи моделі залишаються доступними на всіх рівнях і працюють із тими даними, які були завантажені.
Основні можливості ElasticModel:
- ✅ Повністю наслідує поведінку
pydantic.BaseModel: усі методи та функціональність. Просто заміниBaseModelнаElasticModel. - ✅ Дозволяє створювати моделі з неповними даними й отримувати повний доступ — читати поля, викликати методи, навіть на вкладених моделях.
- ✅ Не потребує великої кількості полів типу
Optional. - ✅ Підтримує динамічні (неописані) поля — вони зберігаються в
self.elastic_extra, словнику для необроблених даних. - ✅ Гарантує контроль доступу: можна звертатись лише до завантажених полів. Спроба доступу до незавантаженого поля викличе
NotLoadedFieldError. - ✅ Підтримує рекурсивне створення вкладених моделей.
- ✅ Дозволяє виконувати поверхневу або глибоку перевірку обов’язкових полів у будь-який момент (на вимогу користувача).
ElasticModel поєднує найкраще з BaseModel.model_validate (структуровані/вкладені моделі) та BaseModel.model_construct (створення без валідації), додаючи нову гнучкість у роботі з частковими даними — без перевантаження кодової бази полями Optional.
pip install gostmodelshttps://pypi.org/project/gostmodels/
💡 ElasticModel.elastic_create validation modes
validate=True- All values are coerced to their declared types and validated by Pydantic. The only exception is nestedElasticModel, which are built with support for "partial data".validate=False- No coercion or validation is applied — values are kept as-is. The only exceptions is nestedBaseModelandElasticModel, which are built with support for "partial data".
from typing import Annotated
from datetime import datetime
from pydantic import Field, EmailStr
from gostmodels import ElasticModel, NotLoadedFieldError
class Created(ElasticModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class User(ElasticModel):
id: str = Field(alias="_id")
first_name: Annotated[str, Field(min_length=2)]
last_name: str
email: EmailStr
created: Created
updated: Created
def welcome(self) -> str:
# Uses only fields present in the example payload below
return f"Hi {self.first_name}! Joined at {self.created.datetime_from_at()}"
# Build from a projection (partial dict)
doc = {
"_id": "u1",
"first_name": "Ann",
"email": "ann@example.com",
"created": {
"at": "2025-08-15"
# "by": missing
},
"updated": {
"at": "2099-01-10"
# "by": missing
},
"external_value": 1, # unknown key → goes to .elastic_extra
}
# --------MAIN CONSTRUCTOR-------
u = User.elastic_create(doc) # ✅ -> ElasticModel
# -------------------------------
assert u.id == "u1" # Alias works; unknown keys preserved without validation
# 💡 .elastic_extra is a simple dict that stores all unknown field models 💡
print(u.elastic_extra) # ✅ -> {'external_value': 1}
# 💡 Nested model is constructed, so methods on nested instances are available
# Model methods can operate with currently loaded data
print(u.created.datetime_from_at()) # ✅ -> "2025-08-15 00:00:00" (type <class 'datetime.datetime')
print(u.welcome()) # ✅ -> "Hi Ann Lee! Joined at 2025-08-15 00:00:00"
# 💡 Accessing a declared but not loaded field → NotLoadedFieldError
print(u.created.by) # ❌ -> ERROR NotLoadedFieldError
# .is_loaded(key) - Safe verification of field presence in the model
assert u.created.elastic_is_loaded("by") == False
u.created.by = "system" # Mark fields as loaded by assigning to them
assert u.created.elastic_is_loaded("by") == True
# 💡 Choose validation depth when you need it
# shallow (recursive=False): do not descend into nested models
ok_shallow, bad_paths = u.elastic_is_valid(recursive=False)
print(ok_shallow, bad_paths) # ✅ -> True, []
# deep (recursive=True): checks nested models and finds missing required field in "updated"
ok_deep, bad_paths = u.elastic_is_valid(recursive=True)
print(ok_deep, bad_paths) # ⚠️ -> False, ['updated.by']
# Produce a fully validated pydantic.BaseModel instance (or raise ValidationError)
u.updated.by = "user" # Before making the pydantic model, we fill in the missing field to avoid getting a ValidationError
validated = u.elastic_get_validated_model(recursive=True) # ✅ -> pydantic.BaseModelfrom datetime import datetime
from pydantic import BaseModel, EmailStr, ValidationError
from gostmodels import ElasticModel
# Compare the methods of creating objects using different approaches:
# 1. pydantic.BaseModel.model_validate
# 2. pydantic.BaseModel.model_construct
# 3. gostmodels.ElasticModel.elastic_create
# Let's create identical BaseModel and ElasticModel model:
# - pydantic.BaseModel
# -------------------------------
class CreatedPydantic(BaseModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class UserPydantic(BaseModel):
email: EmailStr
created: CreatedPydantic
# -------------------------------
# - gostmodels.ElasticModel
# -------------------------------
class CreatedElastic(ElasticModel):
at: str
by: str
def datetime_from_at(self) -> datetime:
return datetime.strptime(self.at, "%Y-%m-%d")
class UserElastic(ElasticModel):
email: EmailStr
created: CreatedElastic
# -------------------------------
# Equally limited data, but enough for the actions we need
partial_data = {
"email": "a@b.com",
"created": {
"at": "2025-08-15"
# "by": missing
}
}
# 1. pydantic.model_validate → raises immediately
user_validate = UserPydantic.model_validate(partial_data) # ❌ -> ERROR ValidationError: 1 validation error for UserPydantic
# 2. pydantic.model_construct → does not validate, but keeps nested dicts
user_construct = UserPydantic.model_construct(**partial_data) # ✅
assert isinstance(user_construct.created, dict) # ⚠️ -> raw dict; methods relying on CreatedPydantic would break
print(user_construct.created.datetime_from_at()) # ❌ -> ERROR AttributeError: 'dict' object has no attribute 'datetime_from_at
# 3. ElasticModel.elastic_create → no instant failures, and nested models are created
user_elastic = UserElastic.elastic_create(partial_data) # ✅
assert isinstance(user_elastic.created, CreatedElastic) # ✅
print(user_elastic.created.datetime_from_at()) # ✅ -> 2025-08-15 00:00:00Summary:
model_validate: full validation + nested building, but no partialsmodel_construct: partials OK, but nested dicts remain dictselastic_create: partials OK + nested building + strict read access + shallow/deep validation
-
Partial construction:
elastic_create(data, validate=True, apply_defaults=False)- Accepts dicts with missing and extra keys
- Validates/coerces values via
TypeAdapterusing your type hints (includingAnnotated[..., Field(...)]) - Unknown keys are captured in
model.elastic_extra - Tracks actually loaded fields in
._loaded_fields apply_defaults=Trueappliesdefault/default_factoryto missing fields and marks them as loaded
-
Strict read access
- Accessing an unloaded declared field raises
NotLoadedFieldError - System attributes and dunders are not intercepted
- Accessing an unloaded declared field raises
-
Shallow vs Deep validation
- Shallow: keep existing nested instances, fast
- Deep: fully materialize to plain structures and validate everything
-
Nested models and containers
- Nested
ElasticModelfields are built viaelastic_create list/set/tupleitems are coerced recursively (whenvalidate=True)dict[K, V]keys and values are validated (whenvalidate=True)
- Nested
ElasticModel.elastic_create(data: dict, *, validate: bool = True, apply_defaults: bool = False) -> Selfmodel.elastic_extra -> dict[str, any]model.elastic_is_loaded(name: str) -> boolmodel.elastic_is_valid(*, recursive: bool = True) -> tuple[bool, list[str]]model.elastic_get_validated_model(recursive: bool = True) -> Self- Assignment marks fields as loaded:
model.field = value
ElasticModel recommends the following default values for pydantic.ConfigDict:
extra='ignore'— extra keys are ignored by Pydantic but manually collected into.elastic_extrarevalidate_instances='never'— nested model instances are not revalidated automatically (important for shallow validation)