Skip to content

Fix: Primitive Fallback for oneOf Deserialization in Device Assurance Models#523

Merged
BinoyOza-okta merged 1 commit intomasterfrom
OKTA-1154600
Apr 12, 2026
Merged

Fix: Primitive Fallback for oneOf Deserialization in Device Assurance Models#523
BinoyOza-okta merged 1 commit intomasterfrom
OKTA-1154600

Conversation

@BinoyOza-okta
Copy link
Copy Markdown
Contributor

@BinoyOza-okta BinoyOza-okta commented Apr 11, 2026

Fix: Primitive Fallback for oneOf Deserialization in Device Assurance Models

Summary

Adds a primitive-fallback retry mechanism to oneOf deserialization so that bare scalar API responses (e.g. "2025-12-31T00:00:00Z") are automatically wrapped into the corresponding single-property schema object. Also fixes a malformed ISO 8601 duration regex that rejected all valid duration strings.

Ticket: OKTA-1154600

Problem

The Okta API returns a raw primitive string for the expiry field inside Device Assurance policy responses, but the SDK's GracePeriodExpiry model expects a JSON object matching one of its oneOf schemas (ByDateTimeExpiry or ByDurationExpiry). This caused every call to the Device Assurance endpoints to fail:

ValidationError: No match found when deserializing the JSON string into
GracePeriodExpiry with oneOf schemas: ByDateTimeExpiry, ByDurationExpiry.

Affected endpoints:

  • list_device_assurance_policies()
  • get_device_assurance_policy(policy_id)

Additionally, the ISO 8601 duration regex contained incorrect constructs ((?:$) instead of the negative lookahead (?!$), and (?:\d) instead of the positive lookahead (?=\d)), meaning it could never match valid duration strings like P30D or PT1H.

Root Cause

  1. oneOf mismatch — The API returns "2025-12-31T00:00:00Z" (a bare string), but the generated from_json method tries to parse it directly as ByDateTimeExpiry (which expects {"value": "..."}) and fails.
  2. Broken regex(?:$) is a non-capturing group containing end-of-string, not a negative lookahead. It causes the regex to silently fail on every non-empty input.

Solution

Files Changed

  1. openapi/api.yaml

    • Added x-okta-primitive-fallback: true vendor extension on the expiry field of the GracePeriod schema.
    • Fixed the ISO 8601 duration regex on ByDurationExpiry.value:
      # Before
      pattern: ^P(?:$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?:\d)(\d+H)?(\d+M)?(\d+S)?)?$
      # After
      pattern: ^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$
  2. openapi/templates/model_oneof.mustache

    • Added a conditional block guarded by x-okta-primitive-fallback that:
      1. Parses the JSON string as a primitive.
      2. For each non-primitive oneOf schema with exactly one field, wraps the primitive in {field_name: value} and retries validation.
    • This is a generic, reusable template change — any future oneOf field that exhibits the same API behaviour only needs the vendor extension in the spec.
  3. okta/models/grace_period_expiry.py (regenerated)

    • Contains the concrete fallback retry logic for ByDateTimeExpiry and ByDurationExpiry, generated from the updated mustache template.
  4. okta/models/by_duration_expiry.py (regenerated)

    • Regex corrected from (?:$)(?!$) and (?:\d)(?=\d).

Testing

✅ Verified list_device_assurance_policies() returns fully deserialized policy objects
✅ Verified get_device_assurance_policy(policy_id) returns a correctly deserialized policy
✅ ISO 8601 duration strings (P30D, PT1H, P1Y2M3DT4H5M6S) now pass regex validation
✅ No breaking changes — the fallback only activates when the standard deserialization finds zero matches
✅ The mustache template change is backward-compatible; models without x-okta-primitive-fallback are unaffected

How It Works

API Response: "2025-12-31T00:00:00Z"
                    │
                    ▼
    ┌──────────────────────────────┐
    │  Standard oneOf matching     │
    │  Try ByDateTimeExpiry → ✗    │
    │  Try ByDurationExpiry → ✗    │
    │  match == 0                  │
    └──────────────┬───────────────┘
                   │  x-okta-primitive-fallback
                   ▼
    ┌──────────────────────────────┐
    │  Primitive fallback retry    │
    │  Wrap as {"value": "2025-…"} │
    │  Try ByDateTimeExpiry → ✓    │
    │  match == 1                  │
    └──────────────────────────────┘

Fixes

- `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)`).
Copy link
Copy Markdown

@aniket-okta aniket-okta left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security audit, static analysis, and live API verification passed. The primitive-fallback retry logic in the mustache template is correct and backward-compatible. Regex fix for ByDurationExpiry is accurate. Verified against okta-mcp-server.oktapreview.com — policies with BY_DURATION grace period (e.g. PT168H) now deserialize correctly. All 15 CI checks green. Minor: consider adding a CHANGELOG entry for this specific fix and a unit test for the fallback path in a follow-up.

@BinoyOza-okta
Copy link
Copy Markdown
Contributor Author

CHANGELOG will be added in the release changes.

@BinoyOza-okta BinoyOza-okta merged commit 1eb3cc9 into master Apr 12, 2026
15 checks passed
@BinoyOza-okta BinoyOza-okta deleted the OKTA-1154600 branch April 12, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants