From a3bfe311efbf3635173f683f79bbae84cc615dec Mon Sep 17 00:00:00 2001 From: BinoyOza-okta Date: Sun, 12 Apr 2026 02:12:42 +0530 Subject: [PATCH] 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. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)`). --- okta/models/by_duration_expiry.py | 4 +-- okta/models/grace_period_expiry.py | 38 ++++++++++++++++++++++++++ openapi/api.yaml | 3 +- openapi/templates/model_oneof.mustache | 34 +++++++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/okta/models/by_duration_expiry.py b/okta/models/by_duration_expiry.py index b7acd4fc..93a1d6d3 100644 --- a/okta/models/by_duration_expiry.py +++ b/okta/models/by_duration_expiry.py @@ -51,11 +51,11 @@ def value_validate_regular_expression(cls, value): return value if not re.match( - r"^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$", + r"^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$", value, ): raise ValueError( - r"must validate the regular expression /^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$/" + r"must validate the regular expression /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/" ) return value diff --git a/okta/models/grace_period_expiry.py b/okta/models/grace_period_expiry.py index 89b40125..0912bc0d 100644 --- a/okta/models/grace_period_expiry.py +++ b/okta/models/grace_period_expiry.py @@ -130,6 +130,44 @@ def from_json(cls, json_str: str) -> Self: except (ValidationError, ValueError) as e: error_messages.append(str(e)) + # If no match found and the data is a primitive value, retry by wrapping + # it into each oneOf schema's single-property structure. This handles API + # responses where a scalar is returned for a oneOf field whose schemas + # are single-property objects. + if match == 0: + try: + _parsed_value = json.loads(json_str) + except (json.JSONDecodeError, TypeError): + _parsed_value = None + if _parsed_value is not None and isinstance( + _parsed_value, (str, int, float, bool) + ): + _retry_error_messages = [] + # retry ByDateTimeExpiry with wrapped primitive + try: + _model_fields = list(ByDateTimeExpiry.model_fields.keys()) + if len(_model_fields) == 1: + instance.actual_instance = ByDateTimeExpiry.model_validate( + {_model_fields[0]: _parsed_value} + ) + match += 1 + except (ValidationError, ValueError) as e: + _retry_error_messages.append(str(e)) + # retry ByDurationExpiry with wrapped primitive + try: + _model_fields = list(ByDurationExpiry.model_fields.keys()) + if len(_model_fields) == 1: + instance.actual_instance = ByDurationExpiry.model_validate( + {_model_fields[0]: _parsed_value} + ) + match += 1 + except (ValidationError, ValueError) as e: + _retry_error_messages.append(str(e)) + if match > 0: + error_messages = _retry_error_messages + else: + error_messages.extend(_retry_error_messages) + if match > 1: # more than 1 match raise ValueError( diff --git a/openapi/api.yaml b/openapi/api.yaml index f6fd5499..629eb090 100644 --- a/openapi/api.yaml +++ b/openapi/api.yaml @@ -62100,7 +62100,7 @@ components: value: type: string description: A time duration in ISO 8601 duration format. - pattern: ^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$ + pattern: ^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$ CAPTCHAInstance: title: CAPTCHAInstance description: '' @@ -65855,6 +65855,7 @@ components: type: object properties: expiry: + x-okta-primitive-fallback: true oneOf: - $ref: '#/components/schemas/ByDateTimeExpiry' - $ref: '#/components/schemas/ByDurationExpiry' diff --git a/openapi/templates/model_oneof.mustache b/openapi/templates/model_oneof.mustache index 3d281613..6f2152ad 100644 --- a/openapi/templates/model_oneof.mustache +++ b/openapi/templates/model_oneof.mustache @@ -167,6 +167,40 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/isContainer}} {{/composedSchemas.oneOf}} + {{#vendorExtensions.x-okta-primitive-fallback}} + # If no match found and the data is a primitive value, retry by wrapping + # it into each oneOf schema's single-property structure. This handles API + # responses where a scalar is returned for a oneOf field whose schemas + # are single-property objects. + if match == 0: + try: + _parsed_value = json.loads(json_str) + except (json.JSONDecodeError, TypeError): + _parsed_value = None + if _parsed_value is not None and isinstance(_parsed_value, (str, int, float, bool)): + _retry_error_messages = [] + {{#composedSchemas.oneOf}} + {{^isContainer}} + {{^isPrimitiveType}} + {{! Only retry non-primitive, non-container schemas. Primitive oneOf + schemas would have already matched during the initial attempt above. }} + # retry {{{dataType}}} with wrapped primitive + try: + _model_fields = list({{{dataType}}}.model_fields.keys()) + if len(_model_fields) == 1: + instance.actual_instance = {{{dataType}}}.model_validate({_model_fields[0]: _parsed_value}) + match += 1 + except (ValidationError, ValueError) as e: + _retry_error_messages.append(str(e)) + {{/isPrimitiveType}} + {{/isContainer}} + {{/composedSchemas.oneOf}} + if match > 0: + error_messages = _retry_error_messages + else: + error_messages.extend(_retry_error_messages) + {{/vendorExtensions.x-okta-primitive-fallback}} + if match > 1: # more than 1 match raise ValueError("Multiple matches found when deserializing the JSON string into {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages))