Repository: slackapi/python-slack-sdk
Pull request #944 (2021-02-04 05:17:32)

Add unknown kwargs in SCIM / Audit Logs clients
## Summary

This pull request applies improvements to the data classes in #940 #936 . Without this change, the JSON response data parser fails when the server-side introduces a new field to the data structure.

### Category (place an `x` in each of the `[ ]`)

- [x] **slack_sdk.audit_logs (sync/async)** (Web API client)
- [x] **slack_sdk.scim (sync/async)** (Web API client)

## Requirements (place an `x` in each `[ ]`)

- [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [x] I've run `python setup.py validate` after making the changes.


codecov[bot]: # [Codecov](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=h1) Report
> Merging [#944](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=desc) (53f9710) into [main](https://codecov.io/gh/slackapi/python-slack-sdk/commit/77328c10d87846741e2801afce5d7ba1fa6be4dc?el=desc) (77328c1) will **increase** coverage by `0.15%`.
> The diff coverage is `85.71%`.

[![Impacted file tree graph](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/graphs/tree.svg?width=650&height=150&src=pr&token=F0O7meYmtD)](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             main     #944      +/-   ##
==========================================
+ Coverage   85.18%   85.34%   +0.15%     
==========================================
  Files          67       67              
  Lines        5860     5902      +42     
==========================================
+ Hits         4992     5037      +45     
+ Misses        868      865       -3     
```


| [Impacted Files](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [slack\_sdk/scim/v1/user.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL3NjaW0vdjEvdXNlci5weQ==) | `88.39% <83.33%> (+5.05%)` | :arrow_up: |
| [slack\_sdk/audit\_logs/v1/logs.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL2F1ZGl0X2xvZ3MvdjEvbG9ncy5weQ==) | `77.33% <84.61%> (+0.80%)` | :arrow_up: |
| [slack\_sdk/scim/v1/group.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL3NjaW0vdjEvZ3JvdXAucHk=) | `87.80% <85.71%> (-0.77%)` | :arrow_down: |
| [slack\_sdk/scim/v1/internal\_utils.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL3NjaW0vdjEvaW50ZXJuYWxfdXRpbHMucHk=) | `90.47% <100.00%> (ø)` | |
| [slack\_sdk/scim/v1/types.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL3NjaW0vdjEvdHlwZXMucHk=) | `100.00% <100.00%> (ø)` | |
| [slack\_sdk/scim/v1/response.py](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944/diff?src=pr&el=tree#diff-c2xhY2tfc2RrL3NjaW0vdjEvcmVzcG9uc2UucHk=) | `88.94% <0.00%> (+2.10%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=footer). Last update [77328c1...53f9710](https://codecov.io/gh/slackapi/python-slack-sdk/pull/944?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/slack_sdk/audit_logs/v1/logs.py b/slack_sdk/audit_logs/v1/logs.py
index 062287e..82d2418 100644
--- a/slack_sdk/audit_logs/v1/logs.py
+++ b/slack_sdk/audit_logs/v1/logs.py
@@ -6,6 +6,7 @@ class User:
     name: Optional[str]
     email: Optional[str]
     team: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -14,20 +15,29 @@ class User:
         name: Optional[str] = None,
         email: Optional[str] = None,
         team: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.id = id
         self.name = name
         self.email = email
         self.team = team
+        self.unknown_fields = kwargs
 
 
 class Actor:
     type: Optional[str]
     user: Optional[User]
+    unknown_fields: Dict[str, Any]
 
-    def __init__(self, type: Optional[str] = None, user: Optional[User] = None) -> None:
+    def __init__(
+        self,
+        type: Optional[str] = None,
+        user: Optional[User] = None,
+        **kwargs,
+    ) -> None:
         self.type = type
         self.user = User(**user) if isinstance(user, dict) else user
+        self.unknown_fields = kwargs
 
 
 class Location:
@@ -35,6 +45,7 @@ class Location:
     id: Optional[str]
     name: Optional[str]
     domain: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -43,11 +54,13 @@ class Location:
         id: Optional[str] = None,
         name: Optional[str] = None,
         domain: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.type = type
         self.id = id
         self.name = name
         self.domain = domain
+        self.unknown_fields = kwargs
 
 
 class Context:
@@ -55,6 +68,7 @@ class Context:
     ua: Optional[str]
     ip_address: Optional[str]
     session_id: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -63,11 +77,13 @@ class Context:
         ua: Optional[str] = None,
         ip_address: Optional[str] = None,
         session_id: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.location = Location(**location) if isinstance(location, dict) else location
         self.ua = ua
         self.ip_address = ip_address
         self.session_id = session_id
+        self.unknown_fields = kwargs
 
 
 class Details:
@@ -96,6 +112,7 @@ class Details:
     app_previously_resolved: Optional[bool]
     admin_app_id: Optional[str]
     bot_id: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -125,6 +142,7 @@ class Details:
         app_previously_resolved: Optional[bool] = None,
         admin_app_id: Optional[str] = None,
         bot_id: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.name = name
         self.new_value = new_value
@@ -151,6 +169,7 @@ class Details:
         self.app_previously_resolved = app_previously_resolved
         self.admin_app_id = admin_app_id
         self.bot_id = bot_id
+        self.unknown_fields = kwargs
 
 
 class App:
@@ -160,6 +179,7 @@ class App:
     is_directory_approved: Optional[bool]
     is_workflow_app: Optional[bool]
     scopes: Optional[List[str]]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -170,6 +190,7 @@ class App:
         is_directory_approved: Optional[bool] = None,
         is_workflow_app: Optional[bool] = None,
         scopes: Optional[List[str]] = None,
+        **kwargs,
     ) -> None:
         self.id = id
         self.name = name
@@ -177,6 +198,7 @@ class App:
         self.is_directory_approved = is_directory_approved
         self.is_workflow_app = is_workflow_app
         self.scopes = scopes
+        self.unknown_fields = kwargs
 
 
 class Channel:
@@ -187,6 +209,7 @@ class Channel:
     is_org_shared: Optional[bool]
     teams_shared_with: Optional[List[str]]
     original_connected_channel_id: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -198,6 +221,7 @@ class Channel:
         is_org_shared: Optional[bool] = None,
         teams_shared_with: Optional[List[str]] = None,
         original_connected_channel_id: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.id = id
         self.privacy = privacy
@@ -206,6 +230,7 @@ class Channel:
         self.is_org_shared = is_org_shared
         self.teams_shared_with = teams_shared_with
         self.original_connected_channel_id = original_connected_channel_id
+        self.unknown_fields = kwargs
 
 
 class File:
@@ -213,6 +238,7 @@ class File:
     name: Optional[str]
     filetype: Optional[str]
     title: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -221,11 +247,13 @@ class File:
         name: Optional[str] = None,
         filetype: Optional[str] = None,
         title: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.id = id
         self.name = name
         self.filetype = filetype
         self.title = title
+        self.unknown_fields = kwargs
 
 
 class Entity:
@@ -236,6 +264,7 @@ class Entity:
     channel: Optional[Channel]
     file: Optional[File]
     app: Optional[App]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -247,6 +276,7 @@ class Entity:
         channel: Optional[Union[Channel, dict]] = None,
         file: Optional[Union[File, dict]] = None,
         app: Optional[Union[App, dict]] = None,
+        **kwargs,
     ) -> None:
         self.type = type
         self.user = User(**user) if isinstance(user, dict) else user
@@ -259,6 +289,7 @@ class Entity:
         self.channel = Channel(**channel) if isinstance(channel, dict) else channel
         self.file = File(**file) if isinstance(file, dict) else file
         self.app = App(**app) if isinstance(app, dict) else app
+        self.unknown_fields = kwargs
 
 
 class Entry:
@@ -269,6 +300,7 @@ class Entry:
     entity: Optional[Entity]
     context: Optional[Context]
     details: Optional[Details]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -280,6 +312,7 @@ class Entry:
         entity: Optional[Entity] = None,
         context: Optional[Context] = None,
         details: Optional[Details] = None,
+        **kwargs,
     ) -> None:
         self.id = id
         self.date_create = date_create
@@ -288,13 +321,21 @@ class Entry:
         self.entity = Entity(**entity) if isinstance(entity, dict) else entity
         self.context = Context(**context) if isinstance(context, dict) else context
         self.details = Details(**details) if isinstance(details, dict) else details
+        self.unknown_fields = kwargs
 
 
 class ResponseMetadata:
     next_cursor: Optional[str]
+    unknown_fields: Dict[str, Any]
 
-    def __init__(self, *, next_cursor: Optional[str] = None) -> None:
+    def __init__(
+        self,
+        *,
+        next_cursor: Optional[str] = None,
+        **kwargs,
+    ) -> None:
         self.next_cursor = next_cursor
+        self.unknown_fields = kwargs
 
 
 class LogsResponse:
@@ -304,6 +345,7 @@ class LogsResponse:
     error: Optional[str]
     needed: Optional[str]
     provided: Optional[str]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -314,6 +356,7 @@ class LogsResponse:
         error: Optional[str] = None,
         needed: Optional[str] = None,
         provided: Optional[str] = None,
+        **kwargs,
     ) -> None:
         self.entries = [Entry(**e) if isinstance(e, dict) else e for e in entries]
         self.response_metadata = (
@@ -325,3 +368,4 @@ class LogsResponse:
         self.error = error
         self.needed = needed
         self.provided = provided
+        self.unknown_fields = kwargs
diff --git a/slack_sdk/scim/v1/group.py b/slack_sdk/scim/v1/group.py
index cd3001a..d26fd5b 100644
--- a/slack_sdk/scim/v1/group.py
+++ b/slack_sdk/scim/v1/group.py
@@ -1,4 +1,4 @@
-from typing import Optional, List, Union
+from typing import Optional, List, Union, Dict, Any
 
 from .default_arg import DefaultArg, NotGiven
 from .internal_utils import _to_dict_without_not_given, _is_iterable
@@ -7,15 +7,18 @@ from .internal_utils import _to_dict_without_not_given, _is_iterable
 class GroupMember:
     display: Union[Optional[str], DefaultArg]
     value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         *,
         display: Union[Optional[str], DefaultArg] = NotGiven,
         value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.display = display
         self.value = value
+        self.unknown_fields = kwargs
 
     def to_dict(self):
         return _to_dict_without_not_given(self)
@@ -24,15 +27,18 @@ class GroupMember:
 class GroupMeta:
     created: Union[Optional[str], DefaultArg]
     location: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         *,
         created: Union[Optional[str], DefaultArg] = NotGiven,
         location: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.created = created
         self.location = location
+        self.unknown_fields = kwargs
 
     def to_dict(self):
         return _to_dict_without_not_given(self)
@@ -44,6 +50,7 @@ class Group:
     members: Union[Optional[List[GroupMember]], DefaultArg]
     meta: Union[Optional[GroupMeta], DefaultArg]
     schemas: Union[Optional[List[str]], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -53,6 +60,7 @@ class Group:
         members: Union[Optional[List[GroupMember]], DefaultArg] = NotGiven,
         meta: Union[Optional[GroupMeta], DefaultArg] = NotGiven,
         schemas: Union[Optional[List[str]], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.display_name = display_name
         self.id = id
@@ -65,6 +73,7 @@ class Group:
             GroupMeta(**meta) if meta is not None and isinstance(meta, dict) else meta
         )
         self.schemas = schemas
+        self.unknown_fields = kwargs
 
     def to_dict(self):
         return _to_dict_without_not_given(self)
diff --git a/slack_sdk/scim/v1/internal_utils.py b/slack_sdk/scim/v1/internal_utils.py
index d508446..8d2e7e3 100644
--- a/slack_sdk/scim/v1/internal_utils.py
+++ b/slack_sdk/scim/v1/internal_utils.py
@@ -30,7 +30,7 @@ def _to_dict_without_not_given(obj: Any) -> dict:
     dict_value = {}
     given_dict = obj if isinstance(obj, dict) else vars(obj)
     for key, value in given_dict.items():
-        if key == "_additional_fields":
+        if key == "unknown_fields":
             if value is not None:
                 converted = _to_dict_without_not_given(value)
                 dict_value.update(converted)
diff --git a/slack_sdk/scim/v1/types.py b/slack_sdk/scim/v1/types.py
index b65a6cd..db9c744 100644
--- a/slack_sdk/scim/v1/types.py
+++ b/slack_sdk/scim/v1/types.py
@@ -1,4 +1,4 @@
-from typing import Optional, Union
+from typing import Optional, Union, Dict, Any
 
 from .default_arg import DefaultArg, NotGiven
 from .internal_utils import _to_dict_without_not_given
@@ -8,6 +8,7 @@ class TypeAndValue:
     primary: Union[Optional[bool], DefaultArg]
     type: Union[Optional[str], DefaultArg]
     value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -15,10 +16,12 @@ class TypeAndValue:
         primary: Union[Optional[bool], DefaultArg] = NotGiven,
         type: Union[Optional[str], DefaultArg] = NotGiven,
         value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.primary = primary
         self.type = type
         self.value = value
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
diff --git a/slack_sdk/scim/v1/user.py b/slack_sdk/scim/v1/user.py
index cbf2e92..f9a7c15 100644
--- a/slack_sdk/scim/v1/user.py
+++ b/slack_sdk/scim/v1/user.py
@@ -12,6 +12,7 @@ class UserAddress:
     primary: Union[Optional[bool], DefaultArg]
     region: Union[Optional[str], DefaultArg]
     street_address: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -22,6 +23,7 @@ class UserAddress:
         primary: Union[Optional[bool], DefaultArg] = NotGiven,
         region: Union[Optional[str], DefaultArg] = NotGiven,
         street_address: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.country = country
         self.locality = locality
@@ -29,6 +31,7 @@ class UserAddress:
         self.primary = primary
         self.region = region
         self.street_address = street_address
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
@@ -49,15 +52,18 @@ class UserRole(TypeAndValue):
 class UserGroup:
     display: Union[Optional[str], DefaultArg]
     value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         *,
         display: Union[Optional[str], DefaultArg] = NotGiven,
         value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.display = display
         self.value = value
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
@@ -66,14 +72,17 @@ class UserGroup:
 class UserMeta:
     created: Union[Optional[str], DefaultArg]
     location: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         created: Union[Optional[str], DefaultArg] = NotGiven,
         location: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.created = created
         self.location = location
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
@@ -82,14 +91,17 @@ class UserMeta:
 class UserName:
     family_name: Union[Optional[str], DefaultArg]
     given_name: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         family_name: Union[Optional[str], DefaultArg] = NotGiven,
         given_name: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.family_name = family_name
         self.given_name = given_name
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
@@ -98,14 +110,17 @@ class UserName:
 class UserPhoto:
     type: Union[Optional[str], DefaultArg]
     value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
         type: Union[Optional[str], DefaultArg] = NotGiven,
         value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.type = type
         self.value = value
+        self.unknown_fields = kwargs
 
     def to_dict(self) -> dict:
         return _to_dict_without_not_given(self)
@@ -130,7 +145,7 @@ class User:
     timezone: Union[Optional[str], DefaultArg]
     title: Union[Optional[str], DefaultArg]
     user_name: Union[Optional[str], DefaultArg]
-    additional_fields: Union[Dict[str, Any], DefaultArg]
+    unknown_fields: Dict[str, Any]
 
     def __init__(
         self,
@@ -165,7 +180,7 @@ class User:
         timezone: Union[Optional[str], DefaultArg] = NotGiven,
         title: Union[Optional[str], DefaultArg] = NotGiven,
         user_name: Union[Optional[str], DefaultArg] = NotGiven,
-        additional_fields: Union[Dict[str, Any], DefaultArg] = NotGiven,
+        **kwargs,
     ) -> None:
         self.active = active
         self.addresses = (
@@ -217,7 +232,7 @@ class User:
         self.title = title
         self.user_name = user_name
 
-        self._additional_fields = additional_fields
+        self.unknown_fields = kwargs
 
     def to_dict(self):
         return _to_dict_without_not_given(self)


Test patch:
diff --git a/tests/slack_sdk/audit_logs/test_response.py b/tests/slack_sdk/audit_logs/test_response.py
new file mode 100644
index 0000000..74489f9
--- /dev/null
+++ b/tests/slack_sdk/audit_logs/test_response.py
@@ -0,0 +1,108 @@
+import json
+import unittest
+
+from slack_sdk.audit_logs.v1.logs import LogsResponse
+
+from slack_sdk.audit_logs import AuditLogsClient, AuditLogsResponse
+from tests.slack_sdk.audit_logs.mock_web_api_server import (
+    cleanup_mock_web_api_server,
+    setup_mock_web_api_server,
+)
+
+
+class TestAuditLogsClient(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_logs(self):
+        json_data = """{
+  "entries": [
+    {
+      "id": "xxx-yyy-zzz-111",
+      "date_create": 1611221649,
+      "action": "user_login",
+      "new_attribute": "this should be just accepted as unknown attribute",
+      "actor": {
+        "type": "user",
+        "new_attribute": "this should be just accepted as unknown attribute",
+        "user": {
+          "id": "W111",
+          "name": "your name",
+          "email": "foo@example.com",
+          "team": "E111"
+        }
+      },
+      "entity": {
+        "type": "user",
+        "new_attribute": "this should be just accepted as unknown attribute",
+        "user": {
+          "id": "W111",
+          "name": "your name",
+          "email": "foo@example.com",
+          "team": "E111"
+        }
+      },
+      "context": {
+        "new_attribute": "this should be just accepted as unknown attribute",
+        "location": {
+          "type": "workspace",
+          "id": "T111",
+          "new_attribute": "this should be just accepted as unknown attribute",
+          "name": "WS",
+          "domain": "foo-bar-baz"
+        },
+        "ua": "UA",
+        "ip_address": "1.2.3.4",
+        "session_id": 1656410836837
+      }
+    },
+    {
+      "id": "32c68de4-cbfa-4fcb-9780-25fdd5aacf32",
+      "date_create": 1611221649,
+      "action": "user_login",
+      "actor": {
+        "type": "user",
+        "user": {
+          "id": "W111",
+          "name": "your name",
+          "email": "foo@example.com",
+          "team": "E111"
+        }
+      },
+      "entity": {
+        "type": "user",
+        "user": {
+          "id": "W111",
+          "name": "your name",
+          "email": "foo@example.com",
+          "team": "E111"
+        }
+      },
+      "context": {
+        "location": {
+          "type": "workspace",
+          "id": "T111",
+          "name": "WS",
+          "domain": "foo-bar-baz"
+        },
+        "ua": "UA",
+        "ip_address": "1.2.3.4",
+        "session_id": 1656410836837
+      }
+    }
+  ],
+  "response_metadata": {
+    "next_cursor": "xxx",
+    "new_attribute": "this should be just accepted as unknown attribute"
+  },
+  "new_attribute": "this should be just accepted as unknown attribute"
+}
+"""
+        logs = LogsResponse(**json.loads(json_data))
+        self.assertIsNotNone(logs)
+        self.assertIsNotNone(logs.entries[0].unknown_fields.get("new_attribute"))
+        self.assertIsNotNone(logs.response_metadata.unknown_fields.get("new_attribute"))
+        self.assertIsNotNone(logs.unknown_fields.get("new_attribute"))
diff --git a/tests/slack_sdk/scim/test_internals.py b/tests/slack_sdk/scim/test_internals.py
index dc66a1a..d0a27b1 100644
--- a/tests/slack_sdk/scim/test_internals.py
+++ b/tests/slack_sdk/scim/test_internals.py
@@ -14,6 +14,5 @@ class TEstInternals(unittest.TestCase):
     def test_snake_cased(self):
         response_body = """{"totalResults":441,"itemsPerPage":1,"startIndex":1,"schemas":["urn:scim:schemas:core:1.0"],"Resources":[{"schemas":["urn:scim:schemas:core:1.0"],"id":"W111","externalId":"","meta":{"created":"2020-08-13T04:15:35-07:00","location":"https://api.slack.com/scim/v1/Users/W111"},"userName":"test-app","nickName":"test-app","name":{"givenName":"","familyName":""},"displayName":"","profileUrl":"https://test-test-test.enterprise.slack.com/team/test-app","title":"","timezone":"America/Los_Angeles","active":true,"emails":[{"value":"botuser@slack-bots.com","primary":true}],"photos":[{"value":"https://secure.gravatar.com/avatar/xxx.jpg","type":"photo"}],"groups":[]}]}"""
         result = _to_snake_cased(json.loads(response_body))
-        print(result)
         self.assertEqual(result["start_index"], 1)
         self.assertIsNotNone(result["resources"][0]["id"])
diff --git a/tests/slack_sdk/scim/test_response.py b/tests/slack_sdk/scim/test_response.py
new file mode 100644
index 0000000..781f45a
--- /dev/null
+++ b/tests/slack_sdk/scim/test_response.py
@@ -0,0 +1,79 @@
+import json
+import unittest
+
+from slack_sdk.scim import SearchUsersResponse, SCIMResponse
+from slack_sdk.scim.v1.internal_utils import _to_snake_cased
+
+
+class TEstInternals(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def test_snake_cased(self):
+        response_body = """{
+  "totalResults": 441,
+  "itemsPerPage": 1,
+  "startIndex": 1,
+  "schemas": [
+    "urn:scim:schemas:core:1.0"
+  ],
+  "Resources": [
+    {
+      "schemas": [
+        "urn:scim:schemas:core:1.0"
+      ],
+      "id": "W111",
+      "externalId": "",
+      "meta": {
+        "created": "2020-08-13T04:15:35-07:00",
+        "location": "https://api.slack.com/scim/v1/Users/W111",
+        "newAttribute": "this should be just accepted as unknown attribute"
+      },
+      "userName": "test-app",
+      "nickName": "test-app",
+      "name": {
+        "givenName": "",
+        "familyName": "",
+        "newAttribute": "this should be just accepted as unknown attribute"
+      },
+      "displayName": "",
+      "profileUrl": "https://test-test-test.enterprise.slack.com/team/test-app",
+      "title": "",
+      "timezone": "America/Los_Angeles",
+      "active": true,
+      "emails": [
+        {
+          "value": "botuser@slack-bots.com",
+          "primary": true,
+          "newAttribute": "this should be just accepted as unknown attribute"
+        }
+      ],
+      "photos": [
+        {
+          "value": "https://secure.gravatar.com/avatar/xxx.jpg",
+          "type": "photo",
+          "newAttribute": "this should be just accepted as unknown attribute"
+        }
+      ],
+      "groups": [],
+      "newAttribute": "this should be just accepted as unknown attribute"
+    }
+  ],
+  "newAttribute": "this should be just accepted as unknown attribute"
+}
+"""
+        response = SearchUsersResponse(
+            SCIMResponse(
+                url="https://www.example.com",
+                status_code=200,
+                raw_body=response_body,
+                headers={},
+            )
+        )
+        user = response.users[0]
+        self.assertIsNotNone(user.unknown_fields.get("new_attribute"))
+        # the unknown fields need to be also camel-cased
+        self.assertIsNotNone(user.to_dict().get("newAttribute"))