Skip to content

ServerMessageEndOfCallReport can't parse payloads from Voice Agent because camelCase fields aren't honored #18

@pratyushsahay

Description

@pratyushsahay

Description
When receiving a payload from a Vapi Voice Agent, the ServerMessageEndOfCallReport model in the server-sdk-python fails to parse the incoming JSON. The payload uses camelCase field names (e.g., endedReason, endTime, secondsFromStart), but the SDK model expects snake_case (ended_reason, end_time, seconds_from_start).

Although the generated model defines aliases using FieldMetadata(alias="..."), those aliases are not actually used during model validation. As a result, the SDK throws large validation errors stating that required fields are missing.


Example

from vapi_server_sdk.models import ServerMessageEndOfCallReport

msg = {
    "timestamp": 1762133576209,
    "type": "end-of-call-report",
    "endedReason": "customer-ended-call",
    "analysis": {"summary": "...", "successEvaluation": "true"},
    "artifact": {
        "messages": [
            {
                "role": "bot",
                "message": "Hi, this is a test.",
                "time": 1762133501000.0,
                "endTime": 1762133502000.0,
                "secondsFromStart": 1.0,
            }
        ],
        "transcript": "AI: Hi, this is a test."
    }
}

ServerMessageEndOfCallReport.model_validate(msg)

Actual behavior

80 validation errors for ServerMessageEndOfCallReport  
ended_reason  
  Field required [type=missing, input_value={...}, input_type=dict]  
artifact.messages.0.BotMessage.end_time  
  Field required [type=missing, input_value={...}, input_type=dict]  
artifact.messages.0.BotMessage.seconds_from_start  
  Field required [type=missing, input_value={...}, input_type=dict]  
...

Expected behavior
The SDK should correctly parse the Voice Agent’s camelCase JSON payload, populating the corresponding snake_case model fields using Pydantic’s alias handling.


Root cause
The generated models use Fern’s custom FieldMetadata(alias="...") annotations instead of Pydantic’s native Field(alias="...").
Pydantic v2 does not honor those aliases when parsing input (model_validate, __init__), only when serializing output (model_dump(by_alias=True)).

This means that incoming camelCase payloads from the Voice Agent (which follow the documented API schema) cannot be deserialized without manual key remapping.


Workarounds

Convert the incoming JSON keys from camelCase → snake_case before validation:

import re  
from typing import Any, Mapping

CAMEL_RE = re.compile(r'(?<!^)(?=[A-Z])')

def camel_to_snake(name: str) -> str:
    return CAMEL_RE.sub('_', name).lower()

def normalize_keys(obj: Any) -> Any:
    if isinstance(obj, Mapping):
        return {camel_to_snake(k): normalize_keys(v) for k, v in obj.items()}
    elif isinstance(obj, list):
        return [normalize_keys(x) for x in obj]
    else:
        return obj

normalized = normalize_keys(msg)  
ServerMessageEndOfCallReport.model_validate(normalized)

Or manually subclass the model to redeclare fields using pydantic.Field(alias="...").


Proposed fix

  • Update code generation for server-sdk-python to use pydantic.Field(alias=...) rather than FieldMetadata(alias=...), ensuring proper alias resolution during validation.
  • Alternatively, update the SDK’s UncheckedBaseModel to include:
model_config = ConfigDict(populate_by_name=True)

and propagate real Pydantic alias definitions into the generated models.


Environment

  • SDK: vapi/server-sdk-python
  • SDK Version: 1.7.3
  • Pydantic: 2.12.3
  • Python: 3.11
  • Platform: macOS

Impact
Payloads received from Voice Agents fail validation because camelCase fields aren’t recognized. This breaks out-of-the-box use of the server SDK for handling voice agent events.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions