case study · Serasa Experian · 2025
From a week of code to a few hours of config: schema-driven forms at Serasa
Serasa connects people shopping for car insurance with insurers. Every insurer wants its own questions, and every new set of questions used to mean new code and a new deploy. I replaced the hard-coded form with a schema-driven one. Adding an insurer went from at least a week of engineering work to a few hours of configuration, done by product.
- Role
- Frontend Developer
- Partners
- 2 → 10, with 2 more in onboarding
- Onboarding
- ≥1 week → a few hours
- Stack
- Next.js · React · TypeScript · JSON Schema · REST
Context
Serasa’s insurance product works as a lead marketplace. A person fills in a quote request, and Serasa sends that lead to the insurers they choose. The insurers are the partners, and the questions they ask are part of their own sales workflow. They decide what they need to know before they can make an offer.
The flow has three steps:
- About you. A general questionnaire about the car’s owner and the people who might drive it.
- Pick insurers. The user chooses which insurers they want offers from.
- Insurer questions. A questionnaire specific to each insurer they picked.
- step 1About youOwner and possible driverssame for everyone
- step 2Pick insurersWho should send an offersame for everyone
- step 3Insurer questionsDefined by each partnerdifferent per insurerInsurer AInsurer BInsurer C…
Steps 1 and 2 are the same for everyone. Step 3 is different for every partner.
The static form was the right first step
We started with two partners and a hand-written form. Every question was a component in the code, with its own validation and its own wiring.
That was the right call. With two partners, a static form was the cheapest way to find out whether the product worked at all. Building a form engine before anyone had used the flow would have been solving a problem we didn’t have yet.
Where it stopped working
The problem showed up as soon as we started adding partners. Each new insurer brought its own list of questions. Each question meant writing a component, wiring the validation, mapping the answer, testing it, and shipping a deploy. Adding a partner took at least a week.
The form had become the bottleneck for the business. The commercial side could sign partners faster than engineering could onboard them, and every partner request landed in a developer’s queue.
Making the case
Nobody asked for this. I saw the static form was going to cap how many partners we could take on, and I kept raising it with the team: every new partner was going to cost us another week, and the cost would only grow.
The argument wasn’t hard to win, because the problem was visible to everyone. What it needed was someone to name it and propose a concrete way out.
The design
The idea was to stop treating questions as code and start treating them as data.
Questions as a schema
Every partner’s questionnaire is a JSON schema. The front end reads the schema and renders the form from it. Product creates and edits schemas through an onboarding page, which saves them through a backend API to a database. A new partner doesn’t need a front-end deploy at all.
// illustrative
{
"id": "garage",
"type": "radio",
"label": "Does the car stay in a garage overnight?",
"options": ["yes", "no"],
"children": {
"yes": [
{
"id": "garage_where",
"type": "select",
"label": "Where?",
"options": ["home", "work", "both"]
}
]
}
},
{
"id": "zip",
"type": "text",
"label": "ZIP code where the car stays overnight",
"mask": "00000-000",
"validation": { "required": true, "pattern": "^\\d{5}-?\\d{3}$" }
}
Conditional questions as a tree
Insurance questionnaires branch constantly. “Does the car stay in a garage?” only leads to “Where?” if the answer is yes. So each question can have child questions tied to a specific answer, and those children only appear when that answer is selected. The form is a tree, and the renderer walks it.
Validation and masks as configuration
Each question carries its own optional rules: a mask for things like ZIP codes and license plates, and validation as a regex or one of the form library’s built-in validators. Nothing about a single question lives in component code.
One output, many partners
No matter which partner’s questions a user answers, the form produces the same output format. A backend service takes that output and translates it into each partner’s format. The front end never needs to know which insurer it’s talking to.
Product builds forms with the form
The onboarding page that product uses to write a partner’s questions is itself built with the same schema-driven engine. The tool that creates forms is a form. That kept the engine honest, because any limitation we hit, we hit in our own tool first.
Try it: change a label, add an option, or delete a question.
{
"partner": "insurer_c",
"answers": {
"garage": "yes",
"garage_where": "home",
"young_driver": "no"
}
}Results
- Onboarding a partner went from at least a week to a few hours.
- 2 partners became 10, with 2 more in onboarding when I left.
- No deploy per partner. A new questionnaire is a schema in the database, not a release.
- Product owns onboarding. Adding a partner stopped being an engineering task.
What I learned
My first instinct, looking back, was “I would have built it dynamic from the start.” I don’t think that’s right.
The static form validated the business cheaply. With two partners, it was the correct design. The skill that mattered wasn’t knowing how to build a form engine. It was noticing the moment the simple solution stopped being right, and making the case to change it before it slowed the business down.
What I’d do next
Schema versioning. When a partner changes its questions, someone halfway through a quote should finish with the version they started.
Validation before publishing. A broken schema should be caught on the onboarding page, not by a user in production.
A live preview for product. Product could see the exact form a user would get while editing the schema, instead of after saving it.
Stack: Next.js · React · TypeScript · JSON Schema · REST