§ 01Migrating a FastAPI Service to Pydantic V2: Lessons from a 50-Endpoint API

FastAPI has supported Pydantic V2 since version 0.100, released in July 2023. We stayed on Pydantic V1 longer than we should have, and when we finally migrated a production API with 50+ endpoints, the process revealed several gotchas that only show up under real traffic.

Why Migrate?

Pydantic's own benchmarks put V2 at roughly 4 to 50 times faster than V1 for validation. For a high-throughput API, this translates to measurable latency improvements. Our P99 response times dropped by 30% after migration, but context matters: this was a read-heavy API where most endpoints return large lists of objects, so validation and serialization were a big share of request time. Services dominated by database or network time will see much less.

But the migration isn't automatic. Pydantic V2 changed how validation works, how models serialize, and how custom validators are defined. If you just upgrade dependencies and run tests, you'll miss subtle bugs that only appear in production.

Breaking Change #1: model_validate vs parse_obj

Pydantic V1 used parse_obj() to create model instances from dictionaries. V2 renames this to model_validate(). The migration tool can fix most cases automatically, but if you have dynamic model creation (like in a generic CRUD framework), you'll need to update it manually:

The bigger issue is that V2 is stricter about type coercion. In V1, a number sent for a str field was silently converted to a string. In V2, it is rejected with a validation error.

Some of our mobile clients sent numeric IDs and postal codes as JSON numbers rather than strings. After upgrading, those requests started returning 422 validation errors. The fix was to be explicit about where we still wanted the lenient behaviour:

Apply this to the affected models rather than globally, so the stricter V2 behaviour still protects the rest of the API.

Breaking Change #2: Serialization Behavior

Pydantic V2 renamed the serialization methods. The old dict() method is replaced by model_dump(), and json() by model_dump_json():

The catch is that model_dump() returns Python objects such as datetime, UUID and Decimal, not JSON-safe values. Inside FastAPI's own response handling that is fine, because FastAPI encodes the result for you. But wherever the dict leaves that path (cache writes, task-queue payloads, a hand-written json.dumps call) serialization breaks. Use mode='json', or serialize straight to a string:

These failures tend to surface outside the request path, in background jobs and caches, so they are easy to miss in endpoint tests. Search the codebase for every place a model is dumped and passed on.

Breaking Change #3: Custom Validators

Pydantic V2 completely redesigned the validator system. The old @validator decorator is replaced by @field_validator, and the function signature changed:

The key differences:

  • Validators must be class methods (add @classmethod)
  • The decorator is @field_validator, not @validator
  • field_validator runs after Pydantic's own validation by default; use mode='before' to see the raw input, like V1's pre=True

We had custom validators for phone numbers, postal codes, and currency amounts. All of them needed manual updates because the migration tool couldn't handle the logic changes.

Performance Gains

After fixing the breaking changes, we measured the performance impact. For a typical endpoint that validates a request body with 20 fields and returns a response with 50 fields:

  • Request validation: 40% faster
  • Response serialization: 60% faster
  • Overall P99 latency: 30% reduction

The biggest gains were in endpoints that return large lists of objects. Serializing 1000 objects went from 80ms to 25ms.

Migration Strategy

Here's the approach that worked for us:

  • Run the Pydantic migration tool (bump-pydantic) to auto-fix obvious changes
  • Update all custom validators manually
  • Decide where you need V1-style lenient coercion and configure it explicitly
  • Replace dict()/json() with model_dump()/model_dump_json(), using mode='json' wherever the output leaves FastAPI
  • Run the full test suite and fix validation errors
  • Deploy to staging and run load tests to catch performance regressions
  • Monitor error rates closely after production deployment

The migration took about a week for our 50-endpoint API. Most of that time was spent fixing custom validators and testing edge cases.

Should You Migrate?

Pydantic V1 is in maintenance mode, and new FastAPI work targets V2. Plan the migration rather than waiting for it to be forced on you. The performance gains are real, especially for high-throughput APIs.

If you're starting a new project, use Pydantic V2 from the beginning. The API is cleaner, the performance is better, and you won't have to deal with migration later.

For existing projects, plan the migration carefully. Budget time for manual fixes, especially if you have custom validators or complex nested models. And test thoroughly before deploying to production.