How validate-with-resolute can protect you against faulty DB Hydration

We will see how our new package for chaining Result type and Domain Validation protects us against corrupted records.

How validate-with-resolute can protect you against faulty DB Hydration
Photo by Wes Warren / Unsplash

A database row is not a domain object. Converting one into the other is where invalid data quietly slips past your business rules if you let it, and Pydantic provides a method that makes this surprisingly easy to do by accident.

model_construct is a Validation Bypass

model_construct is Pydantic's fast path for building model instances when you "know" the data is already valid. It skips all field validators. Used in a repository, it means any corrupt value sitting in the database — a None in a required field, a malformed email, a string where a number belongs — gets promoted to a fully-formed domain object without complaint:

# before: broken DB row becomes a valid-looking User
return Resolute.from_value(
    User.model_construct(user_id=row.user_id, name=row.name, email=row.email)
)

The Result wrapper here provides no safety. The error path is only reached if the row is missing entirely. A row that exists but contains bad data walks straight through.

image showing a faulty DB entry

Replacing model_construct with .from_()

Since User inherits from Record (validate_with_resolute), it has a .from_() class method that runs the full construction and validation pipeline and returns a Result[User]. One line changes the hydration from silent to explicit:

# after: corrupt row surfaces as a Failure, not a bad object
return User.from_(user_id=row.user_id, name=row.name, email=row.email)

The return type stays Result[User], so nothing changes at the call sites. But now a row with invalid data produces a Failure that propagates up rather than a User that carries corrupted state. The live path in main.py demonstrates this:

faulty_hydration = await user_service.get_user(user_id="3")
if has_errors(faulty_hydration):
    print(faulty_hydration)
    return

If user "3" does not exist or its row is malformed, the error is handled in one place rather than surfacing as a runtime fault somewhere deeper in the callstack.

What To Do With Faulty Records

Once .from_() makes corrupt rows visible, there are three options and the right one depends on why the data is invalid.

If the model changed — a field was added, renamed, or its allowed values narrowed — the records are not corrupt in a business sense, they are outdated. A migration that backfills or transforms the affected columns is the clean solution. The domain model defines what valid looks like now, so the migration target is unambiguous. This is preferable whenever the data can be mechanically repaired without guessing intent.

If the data is genuinely unrecoverable — a required field is missing and no reasonable default exists — deletion is the honest choice, but it warrants a hard look at whether dependent records exist and what cascading that delete implies. In practice this is rare, and "delete it" is often used as a shortcut to avoid thinking through the migration.

The third option, hiding faulty records from users, is a short-term compromise that is sometimes the only viable one in a live system. A repository method that silently skips records failing .from_() keeps the application running without surfacing internal data problems to end users. The risk is that those records are now invisible to the system too — no writes, no updates, no notifications — and they will quietly accumulate unless something is tracking them. If you go this route, log every skipped row with its ID and the validation errors. That log is the work order for a future migration.

The decision order is usually: migrate if possible, delete if the data is genuinely garbage and dependencies allow it, hide as a last resort with explicit tracking.