1+ # The Okta software accompanied by this notice is provided pursuant to the following terms:
2+ # Copyright © 2025-Present, Okta, Inc.
3+ # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
4+ # License.
5+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
6+ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS
7+ # IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8+ # See the License for the specific language governing permissions and limitations under the License.
9+ # coding: utf-8
10+
111{ {> partial_header} }
212
13+ import logging
14+
315import json
416from typing import Dict, Any, Optional
517
@@ -16,6 +28,9 @@ from {{packageName}}.models.saml_application import SamlApplication
1628from { {packageName} }.models.secure_password_store_application import SecurePasswordStoreApplication
1729from { {packageName} }.models.ws_federation_application import WsFederationApplication
1830
31+ logger = logging.getLogger(__name__)
32+
33+
1934class ApplicationJsonConverter:
2035 """
2136 Custom JSON converter/factory for Application that handles null, empty, and unknown signOnMode values.
@@ -48,7 +63,6 @@ class ApplicationJsonConverter:
4863 " SAML_2_0" ,
4964 " SECURE_PASSWORD_STORE" ,
5065 " WS_FEDERATION" ,
51- " MFA_AS_SERVICE" , # Maps to base Application class
5266 }
5367
5468 SIGN_ON_MODE_MAPPING = {
@@ -61,7 +75,6 @@ class ApplicationJsonConverter:
6175 " SAML_2_0" : SamlApplication,
6276 " SECURE_PASSWORD_STORE" : SecurePasswordStoreApplication,
6377 " WS_FEDERATION" : WsFederationApplication,
64- " MFA_AS_SERVICE" : Application, # Explicitly map to base Application
6578 }
6679
6780 @classmethod
@@ -93,10 +106,12 @@ class ApplicationJsonConverter:
93106 elif str(sign_on_mode).upper() in cls.KNOWN_SIGN_ON_MODES:
94107 # Known sign-on mode - map to specific class (or base Application for MFA_AS_SERVICE)
95108 target_type = cls.SIGN_ON_MODE_MAPPING[str(sign_on_mode).upper()]
96- obj_copy["signOnMode"] = str(sign_on_mode).upper() # Normalize for validation
109+ obj_copy["signOnMode"] = str(
110+ sign_on_mode
111+ ).upper() # Normalize for validation
97112 else:
98113 # Unknown sign-on mode - preserve original value and set to None for validation
99- logger.info (
114+ logger.debug (
100115 f"Unknown signOnMode '{ sign_on_mode} ' encountered, "
101116 f"routing to base Application class"
102117 )
@@ -109,17 +124,21 @@ class ApplicationJsonConverter:
109124 instance = target_type.model_validate(obj_copy)
110125
111126 # Validate consistency (in dev/test mode)
112- if isinstance(instance, Application) and not isinstance(instance, type(instance).__bases__[0]):
127+ if isinstance(instance, Application) and not isinstance(
128+ instance, type(instance).__bases__[0]
129+ ):
113130 # Log warning if subclass instantiated but fields seem inconsistent
114131 logger.debug(f"Routed { sign_on_mode} to { type(instance).__name__} ")
115132
116133 # Store the original unknown sign-on mode if present
117134 # For known modes, set to None to indicate no preservation needed
118135 if original_sign_on_mode is not None:
119136 # Use object.__setattr__ to bypass Pydantic's validation
120- object.__setattr__(instance, '_original_sign_on_mode', original_sign_on_mode)
137+ object.__setattr__(
138+ instance, "_original_sign_on_mode", original_sign_on_mode
139+ )
121140 else:
122- object.__setattr__(instance, ' _original_sign_on_mode' , None)
141+ object.__setattr__(instance, " _original_sign_on_mode" , None)
123142
124143 return instance
125144
@@ -133,89 +152,53 @@ class ApplicationJsonConverter:
133152 if value is None:
134153 return None
135154
136- # Use Pydantic's model_dump to serialize the instance
137- # This avoids calling to_dict() which would recurse back to this converter
138155 # Derive from model configuration
139156 excluded_fields = {
140- field_name for field_name, field_info in Application.model_fields.items()
141- if field_info.exclude or (field_info.json_schema_extra and field_info.json_schema_extra.get(' readOnly' ))
157+ field_name
158+ for field_name, field_info in Application.model_fields.items()
159+ if field_info.exclude
160+ or (
161+ field_info.json_schema_extra
162+ and field_info.json_schema_extra.get(" readOnly" )
163+ )
142164 }
143165
144166 _dict = value.model_dump(
145167 by_alias=True,
146168 exclude=excluded_fields,
147169 exclude_none=True,
148- mode=' json' , # Serialize enums to their values
170+ mode=" json" , # Serialize enums to their values
149171 )
150172
151173 # Restore original unknown signOnMode if it was preserved
152- if hasattr(value, '_original_sign_on_mode') and value._original_sign_on_mode is not None:
174+ if (
175+ hasattr(value, "_original_sign_on_mode")
176+ and value._original_sign_on_mode is not None
177+ ):
153178 _dict["signOnMode"] = value._original_sign_on_mode
154179 # Ensure signOnMode is serialized as string (in case mode='json' didn't handle it)
155- elif "signOnMode" in _dict and hasattr(_dict["signOnMode"], ' value' ):
180+ elif "signOnMode" in _dict and hasattr(_dict["signOnMode"], " value" ):
156181 _dict["signOnMode"] = _dict["signOnMode"].value
157182
158- # Handle nested model serialization manually
159- # (Pydantic will handle most of this, but we need to ensure proper to_dict() calls for nested models)
160- if value.accessibility:
161- if not isinstance(value.accessibility, dict):
162- _dict["accessibility"] = value.accessibility.to_dict()
163- else:
164- _dict["accessibility"] = value.accessibility
165-
166- if value.express_configuration:
167- if not isinstance(value.express_configuration, dict):
168- _dict["expressConfiguration"] = value.express_configuration.to_dict()
169- else:
170- _dict["expressConfiguration"] = value.express_configuration
171-
172- if value.licensing:
173- if not isinstance(value.licensing, dict):
174- _dict["licensing"] = value.licensing.to_dict()
175- else:
176- _dict["licensing"] = value.licensing
177-
178- if value.universal_logout:
179- if not isinstance(value.universal_logout, dict):
180- _dict["universalLogout"] = value.universal_logout.to_dict()
181- else:
182- _dict["universalLogout"] = value.universal_logout
183-
184- if value.visibility:
185- if not isinstance(value.visibility, dict):
186- _dict["visibility"] = value.visibility.to_dict()
187- else:
188- _dict["visibility"] = value.visibility
189-
190- if value.embedded:
191- if not isinstance(value.embedded, dict):
192- _dict["_embedded"] = value.embedded.to_dict()
193- else:
194- _dict["_embedded"] = value.embedded
195-
196- if value.links:
197- if not isinstance(value.links, dict):
198- _dict["_links"] = value.links.to_dict()
199- else:
200- _dict["_links"] = value.links
183+ # NOTE: Pydantic's model_dump(mode='json') handles most nested BaseModel serialization.
184+ # However, subclass-specific fields (settings, credentials, name) need manual handling
185+ # because they're not in the base Application schema and model_dump doesn't call
186+ # their custom to_dict() methods which may have additional logic.
201187
202- # Handle subclass-specific fields
203- # Check if instance has settings (for subclasses like ActiveDirectoryApplication)
204- if hasattr(value, 'settings') and value.settings:
188+ # Handle subclass-specific fields that may have custom serialization logic
189+ if hasattr(value, "settings") and value.settings:
205190 if not isinstance(value.settings, dict):
206191 _dict["settings"] = value.settings.to_dict()
207192 else:
208193 _dict["settings"] = value.settings
209194
210- # Check if instance has credentials (for subclasses like AutoLoginApplication)
211- if hasattr(value, 'credentials') and value.credentials:
195+ if hasattr(value, "credentials") and value.credentials:
212196 if not isinstance(value.credentials, dict):
213197 _dict["credentials"] = value.credentials.to_dict()
214198 else:
215199 _dict["credentials"] = value.credentials
216200
217- # Check if instance has name (for various subclasses)
218- if hasattr(value, 'name') and value.name:
201+ if hasattr(value, "name") and value.name:
219202 _dict["name"] = value.name
220203
221204 return _dict
0 commit comments