Skip to content

Feature: Add an option to generate exceptions for Conjure errors#1306

Open
wsuppiger wants to merge 9 commits into
developfrom
wsuppiger/errors-fix
Open

Feature: Add an option to generate exceptions for Conjure errors#1306
wsuppiger wants to merge 9 commits into
developfrom
wsuppiger/errors-fix

Conversation

@wsuppiger

@wsuppiger wsuppiger commented Jun 3, 2026

Copy link
Copy Markdown

branch off: #1157

For issue: #910

After this PR

The resulting error looks like:

Conjure:

types:
  definitions:
    default-package: com.palantir.product
    errors:
      DatasetNotFound:
        namespace: Datasets
        code: NOT_FOUND
        docs: Thrown when the requested dataset does not exist
        safe-args:
          datasetRid:
            type: string
            docs: The resource identifier of the dataset that was requested.
          availableDatasets: list<string>

Python:

class product_DatasetNotFound(Exception):
    """Thrown when the requested dataset does not exist
    """

    ERROR_CODE = "NOT_FOUND"
    ERROR_NAMESPACE = "Datasets"
    ERROR_NAME = "Datasets:DatasetNotFound"

    def __init__(self, dataset_rid: str, available_datasets: List[str], error_instance_id: Optional[str] = None) -> None:
        self._dataset_rid = dataset_rid
        self._available_datasets = available_datasets
        self.error_instance_id = error_instance_id if error_instance_id is not None else str(uuid.uuid4())
        super().__init__(self.ERROR_NAME)

    @builtins.property
    def dataset_rid(self) -> str:
        return self._dataset_rid

    @builtins.property
    def available_datasets(self) -> List[str]:
        return self._available_datasets

    def encode(self) -> Dict[str, Any]:
        return {
            'errorCode': self.ERROR_CODE,
            'errorName': self.ERROR_NAME,
            'errorInstanceId': self.error_instance_id,
            'parameters': {
                'datasetRid': ConjureEncoder.do_encode(self.dataset_rid),
                'availableDatasets': ConjureEncoder.do_encode(self.available_datasets)
            }
        }

    @builtins.staticmethod
    def _decode_parameter(decoder: ConjureDecoder, value: Any, conjure_type: Any) -> Any:
        if isinstance(value, str) and conjure_type is not str:
            try:
                value = json.loads(value)
            except (ValueError, TypeError):
                pass
        return decoder.decode(value, conjure_type)

    @builtins.classmethod
    def decode(cls, error: Dict[str, Any]) -> 'product_DatasetNotFound':
        if error.get('errorName') != cls.ERROR_NAME:
            raise ValueError(f"Error '{error.get('errorName')}' is not a {cls.ERROR_NAME}")
        decoder = ConjureDecoder()
        parameters = error.get('parameters', {})
        return cls(
            dataset_rid=cls._decode_parameter(decoder, parameters.get('datasetRid'), str),
            available_datasets=cls._decode_parameter(decoder, parameters.get('availableDatasets'), List[str]),
            error_instance_id=error.get('errorInstanceId')
        )


product_DatasetNotFound.__name__ = "DatasetNotFound"
product_DatasetNotFound.__qualname__ = "DatasetNotFound"
product_DatasetNotFound.__module__ = "package_name.product"

Server side — create / serialize:

from my_package.product import DatasetNotFound

# raise it like any other exception
raise DatasetNotFound(
    dataset_rid="ri.foundry.main.dataset.123",
    available_datasets=["ri.foundry.main.dataset.456"],
)

# or serialize to the Conjure SerializableError envelope
payload = DatasetNotFound(
    dataset_rid="ri.foundry.main.dataset.123",
    available_datasets=["ri.foundry.main.dataset.456"],
).encode()
# {
#   'errorCode': 'NOT_FOUND',
#   'errorName': 'Datasets:DatasetNotFound',
#   'errorInstanceId': '<uuid>',
#   'parameters': {
#       'datasetRid': 'ri.foundry.main.dataset.123',
#       'availableDatasets': ['ri.foundry.main.dataset.456'],
#   },
# }

Client side — deserialize:

from my_package.product import DatasetNotFound

# `payload` is a Conjure SerializableError (e.g. an error response body)
error = DatasetNotFound.decode(payload)
error.dataset_rid         # 'ri.foundry.main.dataset.123'
error.available_datasets  # ['ri.foundry.main.dataset.456']
error.error_instance_id   # '<uuid>'

# decode() raises ValueError if the payload's errorName doesn't match
# and tolerates the legacy stringified parameter format

Also placed a follow-up item for using the enum for ErrorCode after palantir/conjure-python-client#171.

@changelog-app

changelog-app Bot commented Jun 3, 2026

Copy link
Copy Markdown

Successfully generated changelog entry!

Entry generated via PR title

To modify this entry, edit PR title using proper format.


📋Changelog Preview

✨ Features

  • Add an option to generate exceptions for Conjure errors (#1306)

Comment thread conjure-python-core/src/test/resources/errors/example-errors.yml
@wsuppiger wsuppiger requested a review from dtobin June 18, 2026 14:44

@dtobin dtobin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left a couple more comments.

Separately, it might be helpful to include some code snippets in the PR description of how you expect the generated error classes to be used. This has already diverged from the original issue #910, which is okay. If we want these classes to be useful both on the server (creating) and client (deserializing) side, we should include examples of both.

emitConstructor(poetWriter);
emitEncode(poetWriter);
emitDecode(poetWriter);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we define the args as properties as well?

poetWriter.increaseIndent();
for (PythonField field : args) {
poetWriter.writeIndentedLine(String.format(
"%s=decoder.decode(parameters.get('%s'), %s),",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There's some complexity here with how non-string error parameters were serialized historically, see palantir/conjure-java#2608 for a decent explanation. I believe this is still how servers serialize the parameters if you don't provide the Accept-Conjure-Error-Parameter-Format header (or if they don't support it). We should decide how we want to handle this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added support for legacy decoding

safe-args:
inputTokenCount: integer
maxTokens: integer
docs: The supplied input exceeded the model's maximum context window.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This example seemed a little unnecessary so just deleted in favor of the test_decode_recovers_legacy_stringified_scalar_params, can add it back though if you think it is valuable

poetWriter.increaseIndent();
for (PythonField field : args) {
poetWriter.writeIndentedLine(String.format(
"%s=decoder.decode(parameters.get('%s'), %s),",

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added support for legacy decoding

@wsuppiger wsuppiger requested a review from dtobin June 29, 2026 04:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants