error-stack RFC: Enhancing Serialization and Deserialization for Reports #5352
indietyp
started this conversation in
Ideas (Libraries)
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
This proposal introduces improvements for serializing and deserializing
error-stackReports. Whileerror-stackcurrently supports serialization, the functionality is limited and rigid. This RFC aims to address these limitations, proposing a more flexible approach to handlingReports during serialization.Motivation
The current serialization process for
Reports loses critical information, making deserialization problematic. This RFC proposes a revised format that retains more information and is designed to be consistent with our existing debug output.Proposed Format
The new format differentiates between two types of objects:
ContextandAttachment. While their serialized forms appear similar, they serve distinct roles. Below is the proposed structure:Context
{ "typeId": "e8007964-c86e-4550-8977-23f3603b1fe4", // further explained below; can be null "message": "Internal Server Error", // the `Display` output of the `Context` "details": {}, // result of serde::Serialize "attachments": [], "opaqueAttachments": 0, // count of attachments that could not be serialized "children": [] // child contexts }Attachment
{ "typeId": "5c197aee-489d-4269-a3e5-0e02604a078d", "message": "", // optional `Display` output or `DebugHook` output "details": {}, // result of serde::Serialize "shared": false // indicates if the attachment is part of multiple contexts }If
messageordetailsis absent, it means those fields don't exist for that object. ThetypeIdhelps during deserialization, acting as a unique identifier for reconstructing objects.For example:
This would serialize to:
Here, the attachment order is preserved. This mirrors the existing
Debugimplementation, where the order is maintained rather than reversed (which might be more intuitive from an implementation perspective).typeIdand Improving Serde HooksPast attempts to move
Debughook behavior toserdehave encountered issues, especially with deserialization. Specifically, important metadata is lost during serialization, making accurate reconstruction impossible.The proposed
typeIdaddresses this. It's akin toAssetId::Uuidin Bevy. For each pair of serde operations, we assign a (constant or non-constant) UUID to identify and register the type. WhileTypeIdcould serve a similar purpose, it isn’t stable across compilations, rendering it unsuitable for storage. A static user UUID solves this problem, albeit with some additional friction, which can be mitigated via a derive macro.The API might look like this:
In this example, the
StableTypeIdtrait provides a stableu128identifier that helps track the type during serialization and deserialization, this is just aUuid, but removing the reliance onUuidallows easier implementations for the end-user. TheHook<T>struct allows users to register custom serialization logic for specific types. Theautomethod automatically handles this for types implementingserde, whilemanualallows for more control over the process.During deserialization, the system checks the
typeIdand attempts to deserialize using the registered deserializer. If deserialization fails (for example, if the type wasn’t registered), the system falls back to a default approach instead of throwing an error.Deserialization Fallback
When deserialization fails due to e.g. a missing or unknown
typeId, we introduce two fallback constructs:AnyAttachmentandAnyContext(name TBD). These types capture the data that couldn't be deserialized:This ensures deserialization never fails entirely, and users can still retrieve the available data. We could then provide an API like
AnyContext::downcast<T>whereT: serde::Deserializeto attempt a downcast to the appropriate type.In cases where the last
Contextis unknown an end-user could fall back to anAnyReport, which is a simply an alias toReport<AnyContext>. While this potentially loses some information, it allows to reconstruct the most essential parts.Improving the API for Lossy Serialization
Currently, one major downside is the inflexibility of serializing attachments. For every new capability, such as serialization or printing, we would need to create new combinations of methods like
attach_printableorattach_serializable. As a result, the number of possible combinations grows exponentially, leading to unsustainable API design, and confusion to the end-user.This serialization is considered lossy, because unlike the hook implementation there is no requirement for a type id, making it impossible to deserialize it with the correct type. They would still be deserializable, but as a
AnyAttachmentinstead ofT.To simplify this, we propose a new
Attachment<T>type. This abstraction allows the user to attach data with flexible capabilities without needing multiple API combinations. The newAttachmenttype would work as follows:With this change, the
Report::attachmethod would have the signature:The
Attachmentstruct would provide methods for adding capabilities like printing or serialization in a chainable manner:Here’s an example of how this change reflects itself in the API:
Furthermore, to maintain backward compatibility and ease of use
attach_printablewill remain available and will not be deprecated:Future Possibilities
While I am usually not a fan of life before main, one could imagine a derive macro like
thiserror::Error, that also registers the context automatically via a serde hook. This also the pro of us being able to support thingsthiserrorisn't planning any time soon, like propergeneric_member_accesssupport. This is definitely not needed in this version, but something to think about in potentially a future version, it could look like:Beta Was this translation helpful? Give feedback.
All reactions