model_config vs class Config in Pydantic v2

Key takeaways:

  • Replace the inner class Config with model_config = ConfigDict(...).
  • orm_mode becomes from_attributes; allow_population_by_field_name becomes populate_by_name.
  • json_encoders is replaced by field or model serializers.
  • BaseSettings uses SettingsConfigDict.
  • Confirm the configured behavior still applies after the change.

This is a focused slice of the Pydantic V2 Migration Guide.

The Problem This Solves

Configuration moved from an inner class to a typed dict, and several keys were renamed. A mechanical find-and-replace misses the renames, so a model can appear migrated while silently losing a setting like ORM compatibility. This guide maps the changes precisely.

Prerequisites

  • Pydantic v2; pydantic-settings for settings models.

Step-by-Step Implementation

1. The basic swap

# Pydantic v1
class User(BaseModel):
    id: int
    class Config:
        orm_mode = True
        allow_population_by_field_name = True
# Pydantic v2
from pydantic import BaseModel, ConfigDict


class User(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,        # was orm_mode
        populate_by_name=True,       # was allow_population_by_field_name
    )
    id: int

2. Replacing json_encoders

from datetime import datetime

from pydantic import BaseModel, field_serializer


class Event(BaseModel):
    at: datetime

    @field_serializer("at")          # Replaces v1 Config.json_encoders.
    def ser_at(self, value: datetime) -> str:
        return value.isoformat()

3. Settings models

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    # SettingsConfigDict for BaseSettings; ConfigDict for plain models.
    model_config = SettingsConfigDict(env_prefix="APP_", extra="forbid")
    database_url: str

Edge Cases and Gotchas

  • Silent key drops. A renamed key left under the old name is simply ignored; grep for orm_mode, allow_population_by_field_name, json_encoders.
  • extra behavior. v2's defaults around extra fields differ; set extra explicitly where it matters.
  • Mixed styles. Do not leave some models on class Config and others on model_config; standardize.

Verification

def test_from_attributes_works():
    class Row:
        id = 7
    assert User.model_validate(Row()).id == 7   # from_attributes in effect.