Introduction: When You Don’t Know if You Should Validate

In everyday software development, many engineers find themselves asking the same question: “Do I need to validate this data again, or can I assume it’s already valid?”

Sometimes, the answer feels uncertain. One part of the code performs validation “just in case,” while another trusts the input, leading to either redundant checks or dangerous omissions. This situation creates tension between performance and safety, and often results in code that is both harder to maintain and more error-prone.

In this article, I want to examine this common problem from a design perspective and show how it’s related to a deeper architectural issue: a violation of the Liskov Substitution Principle (LSP), one of the core tenets of object-oriented design.

But beyond that, I will propose a structural solution — based on leveraging types as contracts of validity — that shifts the responsibility of validation from the developer to the system itself.

The Trouble with Data Validation

When designing a system, it’s common to introduce validation logic to ensure that a given data structure meets certain constraints. Formally, we might say: we receive some input, validate that its values fall within a defined domain of acceptable values, and then proceed. Later in the program, the same structure may be validated again, either defensively or out of uncertainty. If the data hasn’t changed, this revalidation is redundant.

While validation may impact performance, the more significant issue is ambiguity: Who is responsible for ensuring that the data is valid at any given point in the program?

This uncertainty creates friction in both directions:

Both scenarios result in fragile systems. A function may receive data that violates preconditions, not because the validation failed, but because it was silently omitted. Over time, this inconsistency becomes a source of bugs and unreliable behavior.

What seems like a minor technical detail — “just a simple check” — is actually a structural weakness in how the program models trust and correctness.

The Liskov Violation

This ambiguity around validation responsibility is more than just a code hygiene problem — it’s a design flaw. More precisely, it often leads to a violation of the Liskov Substitution Principle (LSP), one of the foundational ideas in object-oriented programming.

LSP states that objects of a subclass should be substitutable for objects of a superclass without altering the correctness of the program. That is, if a method expects an instance of class Parent, it should work correctly with any Child : Parent, without needing to know the exact type.

Now consider the following pattern, which may look familiar:

class Parent { ... }
class Child : Parent { ... }
...
// somewhere
void processValidObject(Parent parent) {
    if (parent is Child) {
        // process
    } else {
        // error
    }
}

This is a textbook LSP violation. The method claims to accept any Parent, but in reality, it only works if the object is a Child. The contract implied by the method signature is broken, and the type system no longer tells the truth.

In validation terms, the same mistake is often made implicitly: a method declares that it accepts a generic structure (e.g., User, InputData, Request), but silently assumes that this structure has already passed some validation process — without enforcing it or expressing it in the type system.

As a result, invalid inputs creep into places where they were never meant to go, and the program becomes reliant on undocumented, non-local assumptions.

A Better Way: Validity Contracts and Subtypes

Instead of relying on documentation or developer discipline to enforce validation, we can model validity explicitly in the type system. The core idea is simple:

If a certain structure can be either valid or invalid, then the type representing the “valid” version should be distinct and separate.

For example, imagine you have a generic InputData type. Rather than passing it around and hoping it's valid, you can define a subtype — say, ValidatedInput — which represents data that has already passed all checks. The only way to obtain an instance of ValidatedInput is through a dedicated validation function or factory.

This creates a contract of validity:

This approach shifts responsibility:

It’s the same principle behind concepts like non-nullable references, refined types, or units of measure. But here, we’re applying it to domain logic: using the type system to separate raw input from trusted data.

By doing so, we eliminate both redundant validation and unsafe assumptions. We no longer ask, “Should I validate this again?” — because the type system enforces the answer.

Real-World Examples: Files, Access, and the Danger of Assumptions

To illustrate how this principle works in practice, let’s consider a common example: file access.

Imagine a function that takes a File object and is supposed to read from it. Simple enough — but what does the File object really represent? Is it:

If the type is just File, all of these interpretations are possible, and developers may start adding conditional logic like:

fun processFile(file: File) {
    if (file.exists() && file.canRead()) {
        // read file
    } else {
        // log or throw
    }
}

Once again, the function is claiming to handle any File, but actually assumes a subset of files that are already valid for reading. This leads to repeated checks, unclear responsibilities, and potentially missed edge cases.

Now consider an alternative: introduce a type ReadableFile that can only be created by a factory method like:

fun tryMakeReadable(file: File): ReadableFile?

This factory encapsulates all the necessary validation (existence, permissions, etc.). Any function that receives a ReadableFile can proceed without additional checks, because the precondition has been promoted to the type level.

This approach scales naturally to more complex systems:

Each of these becomes a type whose very existence guarantees a set of invariants, eliminating entire classes of bugs caused by misplaced trust.

Engineering Lessons and Takeaways

The problem of repeated or missing validation is not just a nuisance — it’s a sign that the system doesn’t express its assumptions clearly. When validity is implicit, every part of the code must remain paranoid, constantly checking and re-checking conditions that may or may not be satisfied.

By elevating validity to a type-level concern, we give ourselves a powerful tool:

This design pattern is applicable across languages and paradigms. Whether implemented through sealed classes, wrapper types, smart constructors, or dependent types, the goal is the same: encode the program’s invariants in the types themselves.

And in doing so, we honor the Liskov Substitution Principle — not by remembering to follow it, but by making it impossible to break accidentally.

Ultimately, good software design is not just about correctness. It’s about making the right thing easy and the wrong thing hard. Using types to model validation is one of the most effective ways to achieve that goal.

If you enjoyed this article, you might like my book Safe by Design: Explorations in Software Architecture and Expressiveness. It dives deeper into topics like this one — contracts, type safety, architectural clarity, and the philosophy behind better code.

👉 Check it out on GitHub