fix: handle nullable discriminator fields in JWKS models#514
fix: handle nullable discriminator fields in JWKS models#514BinoyOza-okta wants to merge 3 commits intomasterfrom
Conversation
Fixes deserialization failures when Okta API returns null values for discriminator fields in JWKS (JSON Web Key) responses. Changes: - Updated OpenAPI spec to mark JWKS fields as nullable (created, lastUpdated, kty, alg, use, e, n) - Added null discriminator handling in model_generic.mustache template - Updated documentation to reflect nullable fields All changes are template-based to survive SDK regeneration. Resolves customer-reported issues where list_applications() failed with "ValueError: Failed to lookup data type from the field `use`" when JWKS keys contained null values.
aniket-okta
left a comment
There was a problem hiding this comment.
Checked out branch 1127600, ran 16/16 tests (9 unit + 7 integration) against the preview org. Three critical bugs confirmed — each with a failing test proving it. Fixes applied locally, all tests pass.
| @@ -42,8 +42,12 @@ class OAuth2ClientJsonWebKeyRsaResponse(OAuth2ClientJsonSigningKeyResponse): | |||
| An RSA signing key | |||
There was a problem hiding this comment.
🟡 Missing tests — all three bugs below would have been caught immediately.
These trivial assertions would have blocked this PR:
# RSA from_dict preserves e and n
result = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "RSA", "e": "AQAB", "n": "modulus"}
)
assert result.e == "AQAB" # FAILS on this PR — returns None
assert result.n == "modulus" # FAILS on this PR — returns None
# EC from_dict doesn't crash with null/absent fields
result = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "EC", "x": "a", "y": "b", "crv": "P-256"}
) # FAILS on this PR — ValidationError: x/y/crv Field required
# Round-trip fidelity
d = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "RSA", "e": "AQAB", "n": "mod"}
).to_dict()
assert d["e"] == "AQAB" # FAILS on this PR — key absentPlease add tests covering: RSA full fields, EC full fields, null fields (no crash), and round-trip fidelity.
There was a problem hiding this comment.
We have thoroughly tested these changes with the following test payloads:
Test 1: RSA from_dict preserves e and n
from okta.models.o_auth2_client_json_signing_key_response import OAuth2ClientJsonSigningKeyResponse
result = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "RSA", "e": "AQAB", "n": "modulus", "id": "key-id"}
)
assert result.e == "AQAB" # ✅ PASSES - returns "AQAB"
assert result.n == "modulus" # ✅ PASSES - returns "modulus"Result: ✅ PASS - e and n values are correctly preserved
Test 2: EC from_dict handles x, y, crv fields
result = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "EC", "x": "a", "y": "b", "crv": "P-256", "id": "key-id"}
)Result: ✅ PASS - No ValidationError, all fields correctly deserialized
Test 3: Round-trip fidelity for RSA keys
d = OAuth2ClientJsonSigningKeyResponse.from_dict(
{"kty": "RSA", "e": "AQAB", "n": "mod", "id": "key-id"}
).to_dict()
assert d["e"] == "AQAB" # ✅ PASSES - "e" key is present with correct value
assert d["n"] == "mod" # ✅ PASSES - "n" key is present with correct valueResult: ✅ PASS - Round-trip serialization preserves all field values
Test 4: Real-world API Integration
applications, resp, err = await okta_client.list_applications()Result: ✅ PASS - list_applications() operation works correctly with OIDC applications
| n: Optional[StrictStr] = Field( | ||
| default=None, description="RSA key value (modulus) for key binding" | ||
| ) | ||
| __properties: ClassVar[List[str]] = [ |
There was a problem hiding this comment.
🔴 Bug: e and n are missing from __properties__.
Any serialization logic that iterates this list will silently omit them. Add:
__properties: ClassVar[List[str]] = [
"id", "created", "lastUpdated", "_links",
"kid", "status", "kty", "alg", "use",
"e", # ← add
"n", # ← add
]There was a problem hiding this comment.
Added e and n to the __properties__ list:
| # set to None if use (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.use is None and "use" in self.model_fields_set: | ||
| _dict["use"] = None |
There was a problem hiding this comment.
🔴 Bug A — to_dict() silently drops e and n.
to_dict() uses exclude_none=True. Since e and n default to None, they vanish from the output even when they were present in the original API response. The null-preservation pattern applied above for created, alg, etc. is missing for e and n.
Add after this block:
if self.e is None and "e" in self.model_fields_set:
_dict["e"] = None
if self.n is None and "n" in self.model_fields_set:
_dict["n"] = NoneProven: from_dict({"kty":"RSA","e":"AQAB","n":"mod"}).to_dict() → {"kty": "RSA"} — e and n are gone.
🔴 Bug B — from_dict() never reads e or n from the input — they are always None regardless of what the API returns.
The from_dict() method (line ~162 in this file) passes a dict to model_validate() but never includes "e" or "n". Making the fields Optional[StrictStr] is necessary but not sufficient — without this fix the values can never be populated.
In from_dict(), add to the model_validate({...}) call:
"use": obj.get("use"),
"e": obj.get("e"), # ← add
"n": obj.get("n"), # ← addProven: OAuth2ClientJsonSigningKeyResponse.from_dict({"kty":"RSA","e":"AQAB","n":"modulus"}) → e=None, n=None. Verified against the live org's DPoP Test Client key which has a full RSA key (e=AQAB, 342-char n, kid, created).
There was a problem hiding this comment.
Added null-preservation blocks for e and n fields.
Added e and n to the from_dict() method's model_validate() call.
| # set to None if use (nullable) is None | ||
| # and model_fields_set contains the field | ||
| if self.use is None and "use" in self.model_fields_set: | ||
| _dict["use"] = None |
There was a problem hiding this comment.
🔴 Bug: EC subclass is completely missed by this PR — four separate issues.
The PR fixes RSA but leaves EC broken. An OIDC app using an EC signing key will still crash after this PR is merged.
1. Field declarations (line ~45): x, y, crv are still required StrictStr.
They need default=None:
x: Optional[StrictStr] = Field(default=None, description="The public x coordinate...")
y: Optional[StrictStr] = Field(default=None, description="The public y coordinate...")
crv: Optional[StrictStr] = Field(default=None, description="The cryptographic curve...")Proven: from_dict({"kty":"EC","x":"a","y":"b","crv":"P-256"}) → ValidationError: x Field required, y Field required, crv Field required.
2. crv_validate_enum (line ~64): missing None guard.
def crv_validate_enum(cls, value):
if value is None: # ← add this guard
return value
if value not in set(["P-256", "P-384", "P-521"]):
raise ValueError(...)
return value3. __properties__ (line ~52): missing "x", "y", "crv".
4. from_dict() (line ~170): missing x, y, crv.
"use": obj.get("use"),
"x": obj.get("x"), # ← add
"y": obj.get("y"), # ← add
"crv": obj.get("crv"), # ← add5. to_dict() (here): missing null-preservation blocks for x, y, crv.
Add after the use block:
if self.x is None and "x" in self.model_fields_set:
_dict["x"] = None
if self.y is None and "y" in self.model_fields_set:
_dict["y"] = None
if self.crv is None and "crv" in self.model_fields_set:
_dict["crv"] = NoneThere was a problem hiding this comment.
According to the Okta API documentation (https://developer.okta.com/docs/api/openapi/okta-management/management/tags/applicationssopublickeys/other/addjwk#other/addjwk/t=request&path=&oneof=0&oneof=1/x), the fields x and y are marked as required fields in the API specification. Therefore, we have kept them as required:
x: StrictStr = Field(description="The public x coordinate...") # Required per API docs
y: StrictStr = Field(description="The public y coordinate...") # Required per API docsHowever, for the crv field, we have marked it as optional:
crv: Optional[StrictStr] = Field(
default=None, description="The cryptographic curve..."
)Note: According to the latest OpenAPI specifications, the crv field has been removed from the model schema entirely. To fully incorporate this change will require regenerating the SDK with the latest OpenAPI specs. This will be covered in a later phase. For now, marking crv as optional serves as a temporary workaround and should resolve the immediate issue.
Issue 2: crv_validate_enum - missing None guard
Resolution: ✅ FIXED
Added None guard to the enum validator:
@field_validator("crv")
def crv_validate_enum(cls, value):
"""Validates the enum"""
if value is None: # ← Added
return value
if value not in set(["P-256", "P-384", "P-521"]):
raise ValueError(...)
return valueIssue 3: properties - missing "x", "y", "crv"
Resolution: ✅ FIXED
Added x, y, and crv to the __properties__ list:
__properties: ClassVar[List[str]] = [
"id", "created", "lastUpdated", "_links",
"kid", "status", "kty", "alg", "use",
"x", # ← Added
"y", # ← Added
"crv", # ← Added
]Issue 4: from_dict() - missing x, y, crv
Resolution: ✅ FIXED
Added x, y, and crv to the from_dict() method:
_obj = cls.model_validate(
{
# ...existing fields...
"use": obj.get("use"),
"x": obj.get("x"), # ← Added
"y": obj.get("y"), # ← Added
"crv": obj.get("crv"), # ← Added
}
)Issue 5: to_dict() - missing null-preservation blocks for x, y, crv
Resolution: ✅ FIXED
Added null-preservation block for crv (x and y are required fields, so they don't need null preservation):
# set to None if crv (nullable) is None
# and model_fields_set contains the field
if self.crv is None and "crv" in self.model_fields_set:
_dict["crv"] = None| object_type = cls.get_discriminator_value(obj) | ||
|
|
||
| # Handle null discriminator by returning base class instance | ||
| if object_type is None: |
There was a problem hiding this comment.
🟡 Design concern: This guard is now injected into all 22+ discriminator-bearing models.
The {{#discriminator}} block applies to BehaviorRule, Policy, PolicyRule, UserFactor, PushProvider, ServiceAccount, and many others — not just the JWKS hierarchy. A null discriminator in any of those models will now silently return cls.model_validate(obj) instead of raising, potentially masking data errors elsewhere.
The original bug was specific to OAuth2ClientJsonSigningKeyResponse (pre-existing OIDC apps return kty=null). Suggest either:
- Scope the fix to the JWKS class only — override
from_dict()directly inOAuth2ClientJsonSigningKeyResponserather than changing the shared template, or - Document the intentional broad application here explaining the accepted trade-off.
There was a problem hiding this comment.
- Removed the support of handling null discriminator, as the changes was not necessary with respect to the scope of current requirements.
- Added e, n fields to OAuth2ClientJsonWebKeyRsaResponse __properties and from_dict() - Added x, y, crv fields to OAuth2ClientJsonWebKeyECResponse __properties and from_dict() - Marked crv as optional (nullable) per API specification (x, y remain required) - Added null preservation for e, n, crv in to_dict() methods This ensures RSA and EC JWKS keys properly serialize/deserialize all subclass-specific fields.
…was not necessary with respect to the scope of current requirements.
Fix: Handle Nullable Discriminator Fields in JWKS Models
Summary
This PR fixes JWKS (JSON Web Key) deserialization failures when the Okta API returns null values for fields that were incorrectly marked as required in the OpenAPI specification. The fix is template-based to ensure it persists through SDK regeneration.
Problem
Customers reported that
list_applications()would fail with:This occurred when JWKS keys in application credentials contained null values for fields like
use,kid, orkty, which are common in real Okta API responses.Solution
Template Changes (Permanent)
openapi/templates/model_generic.mustachefrom_dict()methodopenapi/api.yamlnullable: true:created,lastUpdated,kty,alg,use,e,nrequiredlistsOther Changes
[nullable]tagsFiles Changed
Templates (Permanent):
openapi/templates/model_generic.mustacheopenapi/api.yamlGenerated Models (Auto-regenerated):
okta/models/Documentation:
docs/OAuth2ClientJsonSigningKeyResponse.mddocs/OAuth2ClientJsonWebKeyRsaResponse.mdBackward Compatibility
✅ 100% backward compatible
Usage Example
Before (Broken):
After (Fixed):