Skip to content

Preserve key type for Optional defaults#350

Open
gaoflow wants to merge 1 commit into
keleshev:masterfrom
gaoflow:fix-optional-default-key-type
Open

Preserve key type for Optional defaults#350
gaoflow wants to merge 1 commit into
keleshev:masterfrom
gaoflow:fix-optional-default-key-type

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 20, 2026

Copy link
Copy Markdown

Summary

Optional(key, default=...) stringifies the key, so when the optional key is absent the default fills the output dict under str(key), while when the key is present the original-typed key is used. The output key type silently depends on whether the data contained the key.

from schema import Schema, Optional

s = Schema({Optional(5, default="x"): str})
s.validate({})        # -> {'5': 'x'}   (string key!)
s.validate({5: "p"})  # -> {5: 'p'}     (int key)

The same affects None (→ 'None'), float, and bool keys. String keys are unaffected (str("a") == "a"), which is why the existing tests — all using string keys — never caught it.

Cause

Optional.__init__ set self.key = str(self._schema). The default-fill path then writes new[default.key] = ..., so the stringified key reached the output. A present key, by contrast, flows through normally and keeps its type.

Fix

Store the original key object (unwrapping a Literal, whose str() form the old code relied on for description/title keys), mirroring Hook.key = self._schema:

self.key = (
    self._schema.schema
    if isinstance(self._schema, Literal)
    else self._schema
)

Now present and absent paths produce the same key type. Literal and plain string keys are unchanged.

Verification

  • Added test_dict_optional_default_preserves_key_type, asserting that int/None/float keys keep their type when filled from the default and that present/absent agree. It fails before the fix and passes after.
  • Full suite: 121 passed (was 120, no pre-existing failures). ruff check adds no new errors (the pre-existing B023 reports in json_schema are untouched); ruff format --check reports already-formatted.

This pull request was prepared with the assistance of AI, under my direction and review.

Optional(key, default=...) stored self.key as str(self._schema), so when
an optional key was absent the default filled the output under the
stringified key (e.g. '5' or 'None'), while a present key kept its
original type (5, None). The output key type silently depended on whether
the data contained the key.

Store the original key object instead (unwrapping a Literal, whose
str() form the old code relied on), mirroring Hook.key. String keys are
unchanged; int/None/float/bool keys now keep their type whether the key
is present or filled from the default.
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.

1 participant