Skip to content

Commit a3bfe31

Browse files
committed
Problem:
- `list_device_assurance_policies` and `get_device_assurance_policy` failed with a ValidationError when deserializing the `GracePeriodExpiry` field. - The `expiry` field is defined as `oneOf: [ByDateTimeExpiry, ByDurationExpiry]`, but the API returns a raw string like `"2025-12-31T00:00:00Z"` instead of `{"value": "2025-12-31T00:00:00Z"}`. - Additionally, the ISO 8601 duration regex in `ByDurationExpiry` was malformed (`(?:$)` and `(?:\d)` instead of `(?!$)` and `(?=\d)`), causing valid duration strings to be rejected. Root Cause: 1. The oneOf deserializer expects a JSON object matching one of the schemas, but the API returns a bare scalar value for fields with single-property oneOf schemas. 2. The regex used incorrect non-capturing groups instead of lookahead assertions, so it could never match valid ISO 8601 durations. Solution: 1. Added `x-okta-primitive-fallback` vendor extension to the `expiry` field in the OpenAPI spec (`openapi/api.yaml`). 2. Updated the `model_oneof.mustache` template to emit retry logic: when no oneOf schema matches and the input is a primitive, wrap it into each single-property schema and retry validation. 3. Regenerated `okta/models/grace_period_expiry.py` with the new fallback logic. 4. Fixed the ISO 8601 duration regex in both `openapi/api.yaml` and `okta/models/by_duration_expiry.py` (changed `(?:$)` → `(?!$)` and `(?:\d)` → `(?=\d)`).
1 parent b4e620f commit a3bfe31

4 files changed

Lines changed: 76 additions & 3 deletions

File tree

okta/models/by_duration_expiry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ def value_validate_regular_expression(cls, value):
5151
return value
5252

5353
if not re.match(
54-
r"^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$",
54+
r"^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$",
5555
value,
5656
):
5757
raise ValueError(
58-
r"must validate the regular expression /^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$/"
58+
r"must validate the regular expression /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/"
5959
)
6060
return value
6161

okta/models/grace_period_expiry.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,44 @@ def from_json(cls, json_str: str) -> Self:
130130
except (ValidationError, ValueError) as e:
131131
error_messages.append(str(e))
132132

133+
# If no match found and the data is a primitive value, retry by wrapping
134+
# it into each oneOf schema's single-property structure. This handles API
135+
# responses where a scalar is returned for a oneOf field whose schemas
136+
# are single-property objects.
137+
if match == 0:
138+
try:
139+
_parsed_value = json.loads(json_str)
140+
except (json.JSONDecodeError, TypeError):
141+
_parsed_value = None
142+
if _parsed_value is not None and isinstance(
143+
_parsed_value, (str, int, float, bool)
144+
):
145+
_retry_error_messages = []
146+
# retry ByDateTimeExpiry with wrapped primitive
147+
try:
148+
_model_fields = list(ByDateTimeExpiry.model_fields.keys())
149+
if len(_model_fields) == 1:
150+
instance.actual_instance = ByDateTimeExpiry.model_validate(
151+
{_model_fields[0]: _parsed_value}
152+
)
153+
match += 1
154+
except (ValidationError, ValueError) as e:
155+
_retry_error_messages.append(str(e))
156+
# retry ByDurationExpiry with wrapped primitive
157+
try:
158+
_model_fields = list(ByDurationExpiry.model_fields.keys())
159+
if len(_model_fields) == 1:
160+
instance.actual_instance = ByDurationExpiry.model_validate(
161+
{_model_fields[0]: _parsed_value}
162+
)
163+
match += 1
164+
except (ValidationError, ValueError) as e:
165+
_retry_error_messages.append(str(e))
166+
if match > 0:
167+
error_messages = _retry_error_messages
168+
else:
169+
error_messages.extend(_retry_error_messages)
170+
133171
if match > 1:
134172
# more than 1 match
135173
raise ValueError(

openapi/api.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62100,7 +62100,7 @@ components:
6210062100
value:
6210162101
type: string
6210262102
description: A time duration in ISO 8601 duration format.
62103-
pattern: ^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$
62103+
pattern: ^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$
6210462104
CAPTCHAInstance:
6210562105
title: CAPTCHAInstance
6210662106
description: ''
@@ -65855,6 +65855,7 @@ components:
6585565855
type: object
6585665856
properties:
6585765857
expiry:
65858+
x-okta-primitive-fallback: true
6585865859
oneOf:
6585965860
- $ref: '#/components/schemas/ByDateTimeExpiry'
6586065861
- $ref: '#/components/schemas/ByDurationExpiry'

openapi/templates/model_oneof.mustache

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,40 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
167167
{{/isContainer}}
168168
{{/composedSchemas.oneOf}}
169169

170+
{{#vendorExtensions.x-okta-primitive-fallback}}
171+
# If no match found and the data is a primitive value, retry by wrapping
172+
# it into each oneOf schema's single-property structure. This handles API
173+
# responses where a scalar is returned for a oneOf field whose schemas
174+
# are single-property objects.
175+
if match == 0:
176+
try:
177+
_parsed_value = json.loads(json_str)
178+
except (json.JSONDecodeError, TypeError):
179+
_parsed_value = None
180+
if _parsed_value is not None and isinstance(_parsed_value, (str, int, float, bool)):
181+
_retry_error_messages = []
182+
{{#composedSchemas.oneOf}}
183+
{{^isContainer}}
184+
{{^isPrimitiveType}}
185+
{{! Only retry non-primitive, non-container schemas. Primitive oneOf
186+
schemas would have already matched during the initial attempt above. }}
187+
# retry {{{dataType}}} with wrapped primitive
188+
try:
189+
_model_fields = list({{{dataType}}}.model_fields.keys())
190+
if len(_model_fields) == 1:
191+
instance.actual_instance = {{{dataType}}}.model_validate({_model_fields[0]: _parsed_value})
192+
match += 1
193+
except (ValidationError, ValueError) as e:
194+
_retry_error_messages.append(str(e))
195+
{{/isPrimitiveType}}
196+
{{/isContainer}}
197+
{{/composedSchemas.oneOf}}
198+
if match > 0:
199+
error_messages = _retry_error_messages
200+
else:
201+
error_messages.extend(_retry_error_messages)
202+
{{/vendorExtensions.x-okta-primitive-fallback}}
203+
170204
if match > 1:
171205
# more than 1 match
172206
raise ValueError("Multiple matches found when deserializing the JSON string into {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages))

0 commit comments

Comments
 (0)