Repository: a2aproject/a2a-python

Issue:
[Bug]: APIKeySecurityScheme implementation parsing falied in the client
## What happened?

Hello everyone,

I am trying to do an example of an A2A client-server communication with `APIKeySecurityScheme` authentication.

This problem was mentioned at <https://github.com/google-a2a/a2a-python/issues/165>. That issue was closed, but now the problem occurs again.

**Environment**

- Windows 10
- Python 3.13
- `pydantic==2.11.7`
- `a2a-sdk==0.2.8`

---

### Agent Card Scheme

```python
api_key_scheme = {
    'type': 'apiKey',
    'name': 'X-API-KEY',
    'in': 'header'
}

agent_card = AgentCard(
    name='Currency Agent',
    description='Helps with exchange rates for currencies',
    url=f'http://{host}:{port}/',
    version='1.0.0',
    defaultInputModes=CurrencyAgent.SUPPORTED_CONTENT_TYPES,
    defaultOutputModes=CurrencyAgent.SUPPORTED_CONTENT_TYPES,
    capabilities=capabilities,
    skills=[skill],
    securitySchemes={
        "api_key": APIKeySecurityScheme.model_validate(api_key_scheme)
    },
    security=[
        {
            'api_key': []
        }
    ],
)
```

---

### The Issue

The `APIKeySecurityScheme`'s `in` field is using an alias. But when the SDK server does `model_dump` for the `AgentCard`, it doesn't open `by_alias`, which causes the `.well-known/agent.json` endpoint to always return a JSON with `"in_"` instead of `"in"`:

Example returned content:

```json
{
  "capabilities": {"pushNotifications": true, "streaming": true},
  "defaultInputModes": ["text", "text/plain"],
  "defaultOutputModes": ["text", "text/plain"],
  "description": "Helps with exchange rates for currencies",
  "name": "Currency Agent",
  "security": [{"api_key": []}],
  "securitySchemes": {
    "api_key": {
      "in_": "header",
      "name": "X-API-KEY",
      "type": "apiKey"
    }
  },
  "skills": [{
    "description": "Helps with exchange values between various currencies",
    "examples": ["What is exchange rate between USD and GBP?"],
    "id": "convert_currency",
    "name": "Currency Exchange Rates Tool",
    "tags": ["currency conversion", "currency exchange"]
  }],
  "url": "http://xxxx:10000/",
  "version": "1.0.0"
}
```

---

### Relevant Code

```python
class APIKeySecurityScheme(BaseModel):
    """API Key security scheme."""

    description: str | None = None
    in_: In = Field(..., alias='in')
    name: str
    type: Literal['apiKey'] = 'apiKey'

async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
    """Handles GET requests for the agent card endpoint."""
    # The public agent card is a direct serialization of the agent_card
    # provided at initialization.
    return JSONResponse(
        self.agent_card.model_dump(mode='json', exclude_none=True)
    )
```

---

### Impact

This makes the sdk client `A2ACardResolver.get_agent_card` parse the card response unsuccessfully:

```python
agent_card = AgentCard.model_validate(agent_card_data)
```

---

### Relevant Log Output

```shell
pydantic_core._pydantic_core.ValidationError: 7 validation errors for AgentCard
securitySchemes.api_key.APIKeySecurityScheme.in
  Field required [type=missing, input_value={'in_': 'header', 'name':...-KEY', 'type': 'apiKey'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.11/v/missing
securitySchemes.api_key.HTTPAuthSecurityScheme.scheme
  Field required [type=missing, input_value={'in_': 'header', 'name':...-KEY', 'type': 'apiKey'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.11/v/missing
securitySchemes.api_key.HTTPAuthSecurityScheme.type
  Input should be 'http' [type=literal_error, input_value='apiKey', input_type=str]
    For further information visit https://errors.pydantic.dev/2.11/v/literal_error
securitySchemes.api_key.OAuth2SecurityScheme.flows
```


Discussion:
Hello, I am the user that reported [#165](https://github.com/google-a2a/a2a-python/issues/165). I am still facing the problem as they answered me and closed the issue but it's not working with the suggested solution. Hope we have it solved soon🤞
I think I made a mistake in my response in #165, when using `model_validate()`, try passing the parameter [`by_alias=True`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_validate) this should ensure that it uses the alias.
> I think I made a mistake in my response in [#165](https://github.com/google-a2a/a2a-python/issues/165), when using `model_validate()`, try passing the parameter [`by_alias=True`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_validate) this should ensure that it uses the alias.我认为我在 [#165](https://github.com/google-a2a/a2a-python/issues/165) 的回复中犯了一个错误，当使用 `model_validate()` 时，尝试传递参数 `by_alias=True` ，这应该能确保它使用别名。

I’m aware of this solution and use it in my local environment, but I don’t want to apply the same workaround in the production environment. When will you update the SDK to address this issue? Currently, your SDK client does not set `by_alias=True` in `JSONRPCApplication._handle_get_agent_card`.

> Hello, I am the user that reported [#165](https://github.com/google-a2a/a2a-python/issues/165). I am still facing the problem as they answered me and closed the issue but it's not working with the suggested solution. Hope we have it solved soon🤞

Currently, the only solution is to modify the SDK code directly, specifically in jsonrpc_app.py, to use by_alias=True as shown below:

```python
async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
    """
    Handles GET requests for the agent card endpoint.

    Args:
        request: The incoming Starlette Request object.

    Returns:
        A JSONResponse containing the agent card data.
    """
    # The public agent card is directly serialized from the agent_card provided at initialization.
    return JSONResponse(
        self.agent_card.model_dump(mode='json', by_alias=True, exclude_none=True)
    )
```
It would be great if they could update the SDK to implement this fix, so we don't have to manually alter the code in the release environment.
Sorry, I got confused on the specific issue you were having. I made PR #226 to add this field. Can you verify that this version resolves the issue?

> > Hello, I am the user that reported [#165](https://github.com/google-a2a/a2a-python/issues/165). I am still facing the problem as they answered me and closed the issue but it's not working with the suggested solution. Hope we have it solved soon🤞
> 
> Currently, the only solution is to modify the SDK code directly, specifically in jsonrpc_app.py, to use by_alias=True as shown below:
> 
> async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
>     """
>     Handles GET requests for the agent card endpoint.
> 
>     Args:
>         request: The incoming Starlette Request object.
> 
>     Returns:
>         A JSONResponse containing the agent card data.
>     """
>     # The public agent card is directly serialized from the agent_card provided at initialization.
>     return JSONResponse(
>         self.agent_card.model_dump(mode='json', by_alias=True, exclude_none=True)
>     )
> It would be great if they could update the SDK to implement this fix, so we don't have to manually alter the code in the release environment.


> Sorry, I got confused on the specific issue you were having. I made PR [#226](https://github.com/google-a2a/a2a-python/pull/226) to add this field. Can you verify that this version resolves the issue?抱歉，我没能完全理解你遇到的具体问题。我提交了 PR [#226](https://github.com/google-a2a/a2a-python/pull/226) 来添加这个字段。你能验证这个版本是否解决了问题吗？
> 
> > > Hello, I am the user that reported [#165](https://github.com/google-a2a/a2a-python/issues/165). I am still facing the problem as they answered me and closed the issue but it's not working with the suggested solution. Hope we have it solved soon🤞你好，我是那个报告问题的人。他们回复我并关闭了问题，但我仍然遇到同样的问题，建议的解决方案不起作用。希望我们能尽快解决它🤞
> > 
> > 
> > Currently, the only solution is to modify the SDK code directly, specifically in jsonrpc_app.py, to use by_alias=True as shown below:目前，唯一的解决方案是直接修改 SDK 代码，具体是在 jsonrpc_app.py 中使用 by_alias=True，如下所示：
> > async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
> > """
> > Handles GET requests for the agent card endpoint. 处理获取代理卡端点的 GET 请求。
> > ```
> > Args:
> >     request: The incoming Starlette Request object.
> > 
> > Returns:
> >     A JSONResponse containing the agent card data.
> > """
> > # The public agent card is directly serialized from the agent_card provided at initialization.
> > return JSONResponse(
> >     self.agent_card.model_dump(mode='json', by_alias=True, exclude_none=True)
> > )
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > It would be great if they could update the SDK to implement this fix, so we don't have to manually alter the code in the release environment.如果他们能更新 SDK 来实现这个修复，那就太好了，这样我们就不必在发布环境中手动修改代码了。

Yes, I use this in my local environment, and then the agent card can be successfully parsed by the a2a client.



Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 750144cb..c16bce80 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,11 +50,13 @@ testpaths = ["tests"]
 python_files = "test_*.py"
 python_functions = "test_*"
 addopts = "-ra --strict-markers"
-asyncio_mode = "strict"
 markers = [
   "asyncio: mark a test as a coroutine that should be run by pytest-asyncio",
 ]
 
+[tool.pytest-asyncio]
+mode = "strict"
+
 [build-system]
 requires = ["hatchling", "uv-dynamic-versioning"]
 build-backend = "hatchling.build"
diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py
index ff6998cd..0b78cc1c 100644
--- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py
+++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py
@@ -375,7 +375,10 @@ async def _handle_get_agent_card(self, request: Request) -> JSONResponse:
         # The public agent card is a direct serialization of the agent_card
         # provided at initialization.
         return JSONResponse(
-            self.agent_card.model_dump(mode='json', exclude_none=True)
+            self.agent_card.model_dump(
+                exclude_none=True,
+                by_alias=True,
+            )
         )
 
     async def _handle_get_authenticated_extended_agent_card(
@@ -392,7 +395,8 @@ async def _handle_get_authenticated_extended_agent_card(
         if self.extended_agent_card:
             return JSONResponse(
                 self.extended_agent_card.model_dump(
-                    mode='json', exclude_none=True
+                    exclude_none=True,
+                    by_alias=True,
                 )
             )
         # If supportsAuthenticatedExtendedCard is true, but no specific


Test patch:
diff --git a/tests/server/apps/jsonrpc/test_serialization.py b/tests/server/apps/jsonrpc/test_serialization.py
new file mode 100644
index 00000000..ea3da1c0
--- /dev/null
+++ b/tests/server/apps/jsonrpc/test_serialization.py
@@ -0,0 +1,94 @@
+from unittest import mock
+
+import pytest
+from starlette.testclient import TestClient
+
+from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication
+from a2a.types import (
+    APIKeySecurityScheme,
+    AgentCapabilities,
+    AgentCard,
+    In,
+    SecurityScheme,
+)
+from pydantic import ValidationError
+
+
+@pytest.fixture
+def agent_card_with_api_key():
+    """Provides an AgentCard with an APIKeySecurityScheme for testing serialization."""
+    # This data uses the alias 'in', which is correct for creating the model.
+    api_key_scheme_data = {
+        'type': 'apiKey',
+        'name': 'X-API-KEY',
+        'in': 'header',
+    }
+    api_key_scheme = APIKeySecurityScheme.model_validate(api_key_scheme_data)
+
+    agent_card = AgentCard(
+        name='APIKeyAgent',
+        description='An agent that uses API Key auth.',
+        url='http://example.com/apikey-agent',
+        version='1.0.0',
+        capabilities=AgentCapabilities(),
+        defaultInputModes=['text/plain'],
+        defaultOutputModes=['text/plain'],
+        skills=[],
+        securitySchemes={'api_key_auth': SecurityScheme(root=api_key_scheme)},
+        security=[{'api_key_auth': []}],
+    )
+    return agent_card
+
+
+def test_starlette_agent_card_with_api_key_scheme_alias(
+    agent_card_with_api_key: AgentCard,
+):
+    """
+    Tests that the A2AStarletteApplication endpoint correctly serializes aliased fields.
+
+    This verifies the fix for `APIKeySecurityScheme.in_` being serialized as `in_` instead of `in`.
+    """
+    handler = mock.AsyncMock()
+    app_instance = A2AStarletteApplication(agent_card_with_api_key, handler)
+    client = TestClient(app_instance.build())
+
+    response = client.get('/.well-known/agent.json')
+    assert response.status_code == 200
+    response_data = response.json()
+
+    security_scheme_json = response_data['securitySchemes']['api_key_auth']
+    assert 'in' in security_scheme_json
+    assert security_scheme_json['in'] == 'header'
+    assert 'in_' not in security_scheme_json
+
+    try:
+        parsed_card = AgentCard.model_validate(response_data)
+        parsed_scheme_wrapper = parsed_card.securitySchemes['api_key_auth']
+        assert isinstance(parsed_scheme_wrapper.root, APIKeySecurityScheme)
+        assert parsed_scheme_wrapper.root.in_ == In.header
+    except ValidationError as e:
+        pytest.fail(
+            f"AgentCard.model_validate failed on the server's response: {e}"
+        )
+
+
+def test_fastapi_agent_card_with_api_key_scheme_alias(
+    agent_card_with_api_key: AgentCard,
+):
+    """
+    Tests that the A2AFastAPIApplication endpoint correctly serializes aliased fields.
+
+    This verifies the fix for `APIKeySecurityScheme.in_` being serialized as `in_` instead of `in`.
+    """
+    handler = mock.AsyncMock()
+    app_instance = A2AFastAPIApplication(agent_card_with_api_key, handler)
+    client = TestClient(app_instance.build())
+
+    response = client.get('/.well-known/agent.json')
+    assert response.status_code == 200
+    response_data = response.json()
+
+    security_scheme_json = response_data['securitySchemes']['api_key_auth']
+    assert 'in' in security_scheme_json
+    assert 'in_' not in security_scheme_json
+    assert security_scheme_json['in'] == 'header'