Data validation.
Data enters your program from the outside world. Users type input. Files contain data. APIs send responses. How do you know this data is correct? Today we learn to validate data and reject what does not meet requirements.
Data from outside is not trustworthy.
A user might enter an email without an @ sign. A file might have a negative price. An API might return a date in the wrong format.
If your code assumes valid data and gets invalid data, problems happen. Calculations produce wrong results. Databases store garbage. The program crashes.
Invalid data is not just a problem at the point of entry. It propagates. Bad email gets stored. Bad email gets used in reports. Bad email gets sent to other systems. The corruption spreads.
Validation is the practice of checking data against rules before accepting it. Data that passes is valid. Data that fails is rejected.
Validity depends on requirements.
A valid email address has an @ sign and a domain. A valid age is between 0 and 150. A valid price is non-negative. A valid date is in a recognized format.
These are rules. Data is valid if it satisfies all applicable rules. Data is invalid if it violates any rule.
Rules come from the domain. The business tells you: orders must have at least one item. Prices must be in dollars, not negative. User IDs must be unique.
Some rules are universal: types must match, required fields must be present. Some rules are specific to your application.
Know your rules. Document them. Implement them. Enforce them.
Where should you validate? At boundaries.
A boundary is where data enters your system. User input is a boundary. File parsing is a boundary. API responses are a boundary. Database queries returning data is a boundary.
At the boundary, you know nothing about the data. It comes from outside. It might be anything. This is where validation belongs.
Inside the boundary, you can trust the data. It has been validated. Code can work with it without rechecking.
This is the shell and core pattern. The shell handles input and validation. The core assumes valid data. The core is simpler because it does not handle validation.
If you validate at boundaries, you validate once. If you validate everywhere, you duplicate validation logic and might miss spots.
Different kinds of checks for different purposes.
Type validation: is the data the right type? Is it a number, not a string? Is it an object, not null?
Format validation: is the structure correct? Does the email have @ sign? Is the date in ISO format?
Range validation: is the value within acceptable bounds? Is age between 0 and 150? Is price non-negative?
Presence validation: are required fields present? Does the user have an email? Does the order have items?
Consistency validation: do related values make sense together? Is the end date after the start date? Is quantity positive when order exists?
Business validation: does the data satisfy domain rules? Is the user authorized to place this order? Does the product exist?
Layer your validation. Type and format first. Then range and presence. Then business rules.
When data is invalid, reject it immediately.
Fail fast means stopping as soon as you detect a problem. Do not try to continue with bad data. Do not hope the problem will not matter.
If email is invalid, stop. Report the error. Do not store the invalid email and discover the problem later when sending.
Failing fast has benefits. The error is close to its cause. The stack trace points to where validation happened. You know exactly what was wrong.
Failing late hides the cause. You find corrupted data in the database. How did it get there? When? You do not know.
Fail fast makes debugging easier. Fail fast prevents corruption spreading.
When validation fails, communicate clearly.
Good error messages explain what is wrong. "Invalid email: missing @ sign" is better than "Error". "Price cannot be negative" is better than "Bad value".
Error messages should help fix the problem. If a date is in wrong format, say what format is expected. If a field is missing, name the field.
For users: be helpful and non-technical. For developers and logs: be specific and include values.
Collect all validation errors when possible. Returning only the first error means the user must fix and resubmit repeatedly. Returning all errors at once lets them fix everything in one attempt.
But be careful with sensitive data. Do not include passwords or secrets in error messages.
Some conditions must always be true, not just at entry.
An invariant is a condition that must hold throughout the lifetime of data. An account balance cannot be negative. A list must always be sorted. A tree must always be balanced.
Initial validation ensures invariants hold when data is created. But operations might break invariants. Withdrawing money might make balance negative.
Every operation that modifies data must maintain invariants. Before completing the operation, check that invariants still hold.
If an operation would violate an invariant, reject it. Do not perform an operation that would leave the system in an invalid state.
Invariants turn point-in-time validation into continuous validation.
Validation checks if data is acceptable. Sanitization transforms data to make it acceptable.
An email is invalid because it has leading spaces. You could reject it. Or you could trim the spaces and accept the cleaned version.
A phone number has dashes and parentheses. You could require only digits. Or you could strip non-digits and normalize the format.
Sanitization is convenient for users. They do not need to follow exact formats. The system cleans their input.
But sanitization can be dangerous. If you sanitize a SQL injection attempt, you might not do it correctly. You might think you fixed it but still have a vulnerability.
The safest approach: sanitize for convenience, then validate the result. Trim spaces, normalize format, then check the cleaned data against rules.
One layer of validation might fail. Add more layers.
Validate in the user interface. The form checks that email has @. But a malicious user can bypass the form.
Validate in the API handler. The server checks the input. But you might miss a case.
Validate in the database. Constraints ensure data fits the schema. This is a last line of defense.
Each layer catches different failures. UI catches honest mistakes. API catches bypassed UI. Database catches bugs in API validation.
This is defense in depth. Multiple layers of protection. If one fails, another catches the problem.
But do not use this as an excuse for sloppy validation. Each layer should be complete. The layers are backup, not primary.
Let us summarize.
Data from outside is not trustworthy. Validation checks it against rules.
Validate at boundaries where data enters your system.
Different types of validation: type, format, range, presence, consistency, business rules.
Fail fast. Reject invalid data immediately before it spreads.
Give clear error messages that help fix the problem.
Invariants ensure ongoing validity, not just initial validity.
Sanitize for convenience, then validate.
Use defense in depth: multiple layers of validation.
Good validation prevents bad data from corrupting your system.