Skip to content

V1: Key Case Transformation

Ritvik Nag edited this page Jan 20, 2025 · 5 revisions

Overview

This section provides details about key casing and its transformation in the upcoming v1 major release. Additionally, it highlights the v1 opt-in functionality available in v0.33.0.

Default Behavior

When loading JSON, the default behavior is a 1:1 mapping between JSON keys and dataclass fields. For instance:

  • A field named my_str in a dataclass maps directly to a JSON key my_str.

Key Casing Options

KeyCase introduces several key casing transformation options. Shorthand notations (single letters) are available for convenience.

Valid Values

  • C | CAMEL: Converts strings to camel case.
    Example: my_field_namemyFieldName

  • P | PASCAL: Converts strings to Pascal case (upper camel case).
    Example: my_field_nameMyFieldName

  • K | KEBAB: Converts strings to kebab/lisp case.
    Example: myFieldNamemy-field-name

  • S | SNAKE: Converts strings to snake case.
    Example: myFieldNamemy_field_name

  • A | AUTO: Automatically maps JSON keys to dataclass fields by trying all valid transformations at runtime. Results are cached for subsequent lookups.
    Example: My-Field-Namemy_field_name


Overriding the Default Behavior

You can override the default behavior using one of the following methods:

Option 1: Passing key_case When Subclassing

@dataclass
class Test(JSONWizard, key_case='C'):
    my_str: str
    my_bool_test: bool
    my_float: float = 1.23

d = {'myStr': 'test', 'myBoolTest': True}

print(repr(Test.from_dict(d)))
# Output: Test(my_str='test', my_bool_test=True, my_float=1.23)

Option 2: Using Meta Settings

@dataclass
class Test(JSONWizard):

    class _(JSONWizard.Meta):
        v1 = True
        v1_key_case = 'CAMEL'

    my_str: str
    my_bool_test: bool
    my_float: float = 1.23

d = {'myStr': 'test', 'myBoolTest': True, 'MyFloat': 42}

print(repr(Test.from_dict(d)))
# Output: Test(my_str='test', my_bool_test=True, my_float=1.23)

AUTO Key Casing Transform

The AUTO key casing mode (A) enables automatic detection of valid JSON key casing formats. It sequentially tries:

  1. Exact match with the dataclass field.
  2. camelCase.
  3. PascalCase.
  4. kebab-case.
  5. Upper-Kebab.
  6. Upper_Snake.
  7. snake_case.

The transformation result is cached for optimized future lookups.

Example

@dataclass
class Test(JSONWizard, key_case='AUTO'):
    my_str: str
    my_bool_test: bool
    my_int: int
    my_float: float = 1.23

d = {'My-Str': 'test', 'myBoolTest': True, 'MyInt': 123, 'my_float': 42}

print(repr(Test.from_dict(d)))
# Output: Test(my_str='test', my_bool_test=True, my_int=123, my_float=42.0)

Shortcomings

Note

UPDATE: This issue is resolved in #175. This section is outdated and will be updated shortly to reflect that.

There is one known limitation with the AUTO key casing transform and aliases in general. In v1, the library iterates over the dataclass fields instead of the input JSON object o. This design can lead to issues when multiple JSON keys map to a single dataclass field, as illustrated in the following example:

from dataclasses import dataclass
from dataclass_wizard import LoadMeta, JSONWizard, fromdict

@dataclass
class Container:
    id: int
    my_elements: list['MyElement']

@dataclass
class MyElement:
    order_index: int
    status_code: 'int | str'

d = {
    'id': '123',
    'myElements': [
        {'orderIndex': 111, 'statusCode': '200'},
        {'order_index': '222', 'status_code': 404}
    ]
}

LoadMeta(v1=True, v1_key_case='AUTO').bind_to(Container)

# Failure!
c = fromdict(Container, d)

Running the above code results in the following error:

dataclass_wizard.errors.MissingFields: `MyElement.__init__()` is missing required fields.
  - Provided: []
  - Missing: ['order_index', 'status_code']
  - Expected Keys: ['orderIndex', 'statusCode']
  - Input JSON: {"order_index": "222", "status_code": 404}
  - Key Transform: AUTO
Resolution: Ensure that all required fields are provided in the input. For more details, see:
  https://github.com/rnag/dataclass-wizard/discussions/167

This behavior is expected because, with the current v1 implementation, there is no mechanism to map multiple JSON keys to a single dataclass field. As a result, the first key encountered becomes the "alias" for that field.


Next Steps

A test case replicating the example above will be created and marked as a TODO item for future resolution. This serves as a reminder to address the limitation in a future release.