Part 8 – Schema
This is a series of posts I’m writing about using types as another tool in software development, Continuous Delivery, & keeping LLM’s honest. They’re also a design & refactoring tool, a communication tool, and reduce how many tests you have to write.
Parts
- Part 1 – Branded Types
- Part 2 – Product Types
- Part 3 – Union & Discriminated Unions
- Part 4 – Non-Empty Collections
- Part 5 – Indexed Types
- Part 6 – unknown vs any
- Part 7 – Result
- Part 8 – Schema
- Part 9 – Total Function
- Part 10 – Errors as Values
- Part 11 – Property Tests
- Part 12 – Type Proofs
- Part 13 – Exhaustiveness Checking
- Part 14 – Parse, Don’t Validate
- Part 15 – Anti-Corruption Layer
- Part 16 – Opaque Types
- Part 17 – Maybe
- Part 18 – Smart Constructors
- Part 19 – Pipeline
- Part 20 – Railway Oriented Programming
- Part 21 – Typestate
- Part 22 – Capabilities
- Part 23 – Immutability
- Part 24 – Making Impossible States Impossible
- Part 25 – Type Driven Development: How to do it
- Part 26 – Final Thoughts


To do anything of value in software, we have to get unsafe data from outside our type system into it. We saw how unknown forces us to type narrow, but type narrowing is dangerous, tedious, and it’s hard to be thorough.
cow = JSON.parse(json)
if(cow
&& 'age' in cow
&& typeof cow?.age === 'number')
// could be NaNCode language: JavaScript (javascript)
Then comes the architecture problem; how do you DRY (Don’t Repeat Yourself) this? Multiple parts of your code have:
type Cow = { age: number }
But later the back-end changes age to be optional:
type Cow = { age?: number }
So you have to update both your types and parsing code. This occurs a lot, which then creates Shotgun Parsing all throughout your code:
if(cow.age) {
// use age
} else {
// throw an error
}Code language: JavaScript (javascript)
This is because despite types, no one can trust the data. No code knows where the data came from; “Maybe age hasn’t been validated yet? Better validate just in case.” You started coupling the types through your code in hopes everyone would know “what a Cow is”, but now all the code is repeatedly validating the data. Worse, only parts are validated:
if(cow.name) {
// if no name, throws an error
// what about age? Who knowsCode language: JavaScript (javascript)
Even code formatting, e.g. Oxcfmt/Prettier/Biome, is bypassed b/c data is at runtime, so Python snake case data:
{ first_name: 'Moo', last_name: 'Cow' }Code language: CSS (css)
Can start infecting your camelCase style code. Since validation is often impromptu, it’s not all the same or thorough. Empty strings ”, NaN for numbers, and optional properties on data cause runtime errors or null checks like if(thing?.and?.some?.prop) adding lots of validation code repeatedly.
Lastly, the data you get isn’t always what you want. REST API’s can send back 100 JSON values, but you only need 2, forcing you to write & maintain a lot of parsing code you’ll never use. JSON can have optional values, but you require that data, knowingly parsing the thing you don’t like.
Schema’s solve all this. 1 source of truth, it either parses or it doesn’t, the type is validated data all of your code can trust, use without modifying, and not have to validate. The shape can be what you want/need instead of coupling to what the back-end gives you, then having to update all your code when the back-end changes. Zod/ArkType/Effect all provide schemas:
// define your parsing functions
cowSchema = z.object({
// a number that is not NaN and positive
age: z.number().gte(0),
// a string name instead of Python's snake case first_name
name: z.string().transform( d => ({ …d, name: d.first_name }) )
})
// use it to create your type
type Cow = z.infer<typeof cowSchema>
// parse your data
json = JSON.parse(data)
cow = cowSchema.parse(json)
// cow is a Cow type and safe to use!Code language: JavaScript (javascript)
Leave a Reply