cattrs icon indicating copy to clipboard operation
cattrs copied to clipboard

Obtain object/field instance causing validation error

Open jamesbr3 opened this issue 6 months ago • 1 comments

I can't work out how to obtain a reference to the object instance or field causing a validation error when calling cattrs.structure

For example:

from attrs import define
from cattrs import structure, BaseValidationError 


data = [
   {
    'id': 'aaa',
    'title': 'atitle',
   },
   {
     'XX': 'bbb',
     'title': 'btitle'
   }
]


@define
class Example:
    id: str
    title: str

try:
  example = structure(data, list[Example])
except BaseValidationError as e:
  print(f"Exception: {e}")
  example = None

print(example)

In the example above, one of the list items causes an IterableValidationError (as expected), and within the exception there is a ClassValidationError sub-exception (and KeyErrors within that).

The question is, how do I obtain the instance of the object causing the exception? The reasons for needing this information, is that the dictionary will be decorated with additional metadata (line number information) as the dict is a result from parsing with the ruamel.yaml library. I need to have the object instance causing the error as this will give better error messages in my use-case

thanks

jamesbr3 avatar Jun 17 '25 11:06 jamesbr3

Exceptions from (un)structuring should be ExceptionGroup which you should be able to handle to get what you want: https://catt.rs/en/stable/validation.html

salotz avatar Jun 17 '25 13:06 salotz

Hi,

so I think you have a couple of options here. One would be using the cattrs.transform_error helper. In this case, it will produce a list of error messages like this:

['required field missing @ $[1].id']

You could parse the path string after the @ symbol to dig out the actual payload. $ means the root object, [N] means to index into it at N, and .id means the id attribute.

The second option is to take the source of the transform_error function and just copy it into your codebase and adjust it. transform_error is essentially a tree walker (since the exceptions returned form a tree), and not very complex.

Here's what it might look like:

from typing import Any

from attrs import define

from cattrs import BaseValidationError, structure
from cattrs.errors import ClassValidationError, IterableValidationError

data = [{"id": "aaa", "title": "atitle"}, {"XX": "bbb", "title": "btitle"}]


@define
class Example:
    id: str
    title: str


def get_error_leaves(
    exc: ClassValidationError | IterableValidationError | BaseException, root: Any
) -> list[Any]:
    error_payloads = []
    if isinstance(exc, IterableValidationError):
        with_notes, without = exc.group_exceptions()
        for exc, note in with_notes:
            if isinstance(exc, (ClassValidationError, IterableValidationError)):
                error_payloads.extend(get_error_leaves(exc, root[note.index]))
            else:
                error_payloads.append(root)
    elif isinstance(exc, ClassValidationError):
        with_notes, without = exc.group_exceptions()
        for exc, note in with_notes:
            if isinstance(exc, (ClassValidationError, IterableValidationError)):
                error_payloads.extend(get_error_leaves(exc, root[note.name]))
            else:
                error_payloads.append(root)
    else:
        error_payloads.append(root)
    return error_payloads


try:
    example = structure(data, list[Example])
except BaseValidationError as e:
    print(f"Exception: {e}")
    print(get_error_leaves(e, data))
    example = get_error_leaves(e, data)[0]

print(example)

I put this together in five minutes so it's probably not production-quality, just a starting point for you.

Let me know if you have any other questions, will close this now to keep the issue list tidy.

Tinche avatar Jul 14 '25 11:07 Tinche