Skip to content

Pydantic v2 Migration Guide

Overview

Langroid has fully migrated to Pydantic v2! All internal code now uses Pydantic v2 patterns and imports directly from pydantic. This guide will help you update your code to work with the new version.

Compatibility Layer (Deprecated)

If your code currently imports from langroid.pydantic_v1:

# OLD - Deprecated
from langroid.pydantic_v1 import BaseModel, Field, BaseSettings

You'll see a deprecation warning. This compatibility layer now imports from Pydantic v2 directly, so your code may continue to work, but you should update your imports:

# NEW - Correct
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings  # Note: BaseSettings moved to pydantic_settings in v2

BaseSettings Location Change

In Pydantic v2, BaseSettings has moved to a separate pydantic_settings package. You'll need to install it separately: pip install pydantic-settings

Compatibility Layer Removal

The langroid.pydantic_v1 module will be removed in a future version. Update your imports now to avoid breaking changes.

Key Changes to Update

1. All Fields Must Have Type Annotations

Critical Change

In Pydantic v2, fields without type annotations are completely ignored!

# WRONG - Fields without annotations are ignored in v2
class MyModel(BaseModel):
    name = "John"          # ❌ This field is IGNORED!
    age = 25               # ❌ This field is IGNORED!
    role: str = "user"     # ✅ This field works

# CORRECT - All fields must have type annotations
class MyModel(BaseModel):
    name: str = "John"     # ✅ Type annotation required
    age: int = 25          # ✅ Type annotation required
    role: str = "user"     # ✅ Already correct

This is one of the most common issues when migrating to v2. Always ensure every field has an explicit type annotation, even if it has a default value.

Special Case: Overriding Fields in Subclasses

Can Cause Errors

When overriding fields from parent classes without type annotations, you may get actual errors, not just ignored fields!

This is particularly important when creating custom Langroid agent configurations:

# WRONG - This can cause errors!
from langroid import ChatAgentConfig
from langroid.language_models import OpenAIGPTConfig

class MyAgentConfig(ChatAgentConfig):
    # ❌ ERROR: Missing type annotation when overriding parent field
    llm = OpenAIGPTConfig(chat_model="gpt-4")

    # ❌ ERROR: Even with Field, still needs type annotation
    system_message = Field(default="You are a helpful assistant")

# CORRECT - Always include type annotations when overriding
class MyAgentConfig(ChatAgentConfig):
    # ✅ Type annotation required when overriding
    llm: OpenAIGPTConfig = OpenAIGPTConfig(chat_model="gpt-4")

    # ✅ Type annotation with Field
    system_message: str = Field(default="You are a helpful assistant")

Without type annotations on overridden fields, you may see errors like: - ValueError: Field 'llm' requires a type annotation - TypeError: Field definitions should be annotated - Validation errors when the model tries to use the parent's field definition

2. Stricter Type Validation for Optional Fields

Breaking Change

Pydantic v2 is much stricter about type validation. Fields that could accept None in v1 now require explicit Optional type annotations.

# WRONG - This worked in v1 but fails in v2
class CloudSettings(BaseSettings):
    private_key: str = None      # ❌ ValidationError: expects string, got None
    api_host: str = None         # ❌ ValidationError: expects string, got None

# CORRECT - Explicitly mark fields as optional
from typing import Optional

class CloudSettings(BaseSettings):
    private_key: Optional[str] = None    # ✅ Explicitly optional
    api_host: Optional[str] = None       # ✅ Explicitly optional

    # Or using Python 3.10+ union syntax
    client_email: str | None = None      # ✅ Also works

This commonly affects: - Configuration classes using BaseSettings - Fields with None as default value - Environment variable loading where the var might not be set

If you see errors like:

ValidationError: Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]

The fix is to add Optional[] or | None to the type annotation.

3. Model Serialization Methods

# OLD (Pydantic v1)
data = model.dict()
json_str = model.json()
new_model = MyModel.parse_obj(data)
new_model = MyModel.parse_raw(json_str)

# NEW (Pydantic v2)
data = model.model_dump()
json_str = model.model_dump_json()
new_model = MyModel.model_validate(data)
new_model = MyModel.model_validate_json(json_str)

4. Model Configuration

# OLD (Pydantic v1)
class MyModel(BaseModel):
    name: str

    class Config:
        extra = "forbid"
        validate_assignment = True

# NEW (Pydantic v2)
from pydantic import BaseModel, ConfigDict

class MyModel(BaseModel):
    model_config = ConfigDict(
        extra="forbid",
        validate_assignment=True
    )

    name: str

5. Field Validators

# OLD (Pydantic v1)
from pydantic import validator

class MyModel(BaseModel):
    name: str

    @validator('name')
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v

# NEW (Pydantic v2)
from pydantic import field_validator

class MyModel(BaseModel):
    name: str

    @field_validator('name')
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v

6. Custom Types and Validation

# OLD (Pydantic v1)
from pydantic import parse_obj_as
from typing import List

data = [{"name": "Alice"}, {"name": "Bob"}]
users = parse_obj_as(List[User], data)

# NEW (Pydantic v2)
from pydantic import TypeAdapter
from typing import List

data = [{"name": "Alice"}, {"name": "Bob"}]
users = TypeAdapter(List[User]).validate_python(data)

Common Patterns in Langroid

When working with Langroid's agents and tools:

Tool Messages

from pydantic import BaseModel, Field
from langroid.agent.tool_message import ToolMessage

class MyTool(ToolMessage):
    request: str = "my_tool"
    purpose: str = "Process some data"

    # Use Pydantic v2 patterns
    data: str = Field(..., description="The data to process")

    def handle(self) -> str:
        # Tool logic here
        return f"Processed: {self.data}"

Agent Configuration

from pydantic import ConfigDict
from langroid import ChatAgentConfig

class MyAgentConfig(ChatAgentConfig):
    model_config = ConfigDict(extra="forbid")

    custom_param: str = "default_value"

Troubleshooting

Import Errors

If you see ImportError or AttributeError after updating imports: - Make sure you're using the correct v2 method names (e.g., model_dump not dict) - Check that field validators use @field_validator not @validator - Ensure ConfigDict is used instead of nested Config classes

Validation Errors

Pydantic v2 has stricter validation in some cases: - Empty strings are no longer coerced to None for optional fields - Type coercion is more explicit - Extra fields handling may be different

Performance

Pydantic v2 is generally faster, but if you notice any performance issues: - Use model_validate instead of creating models with **dict unpacking - Consider using model_construct for trusted data (skips validation)

Need Help?

If you encounter issues during migration: 1. Check the official Pydantic v2 migration guide 2. Review Langroid's example code for v2 patterns 3. Open an issue on the Langroid GitHub repository