{"record":{"id":"6a2477bfc7a70697","repo":"dotnet/orleans","slug":"unable-to-convert-from-storage-format-grainstateen","errorCode":null,"errorMessage":"Unable to convert from storage format GrainStateEntity.Data={entity.State}Data Value={dataValue} Type={dataValue.GetType()}","messagePattern":"Unable to convert from storage format GrainStateEntity\\.Data=(.+?)Data Value=(.+?) Type=(.+?)","errorType":"exception","errorClass":"AggregateException","httpStatus":null,"severity":"error","filePath":"src/AWS/Orleans.Transactions.DynamoDB/TransactionalState/DynamoDBTransactionalStateStorage.cs","lineNumber":418,"sourceCode":"        }\n    }\n\n    private T ConvertFromStorageFormat<T>(StateEntity entity)\n    {\n        T dataValue = default!;\n        try\n        {\n            if (entity.State is { Length: > 0 })\n                dataValue = this.serializer.Deserialize<T>(entity.State)!;\n        }\n        catch (Exception exc)\n        {\n            var message = dataValue is not null\n                ? $\"Unable to convert from storage format GrainStateEntity.Data={entity.State}Data Value={dataValue} Type={dataValue.GetType()}\"\n                : $\"Unable to convert from storage format GrainStateEntity.Data={entity.State}\";\n\n            LogError(logger, message);\n            throw new AggregateException(message, exc);\n        }\n\n        return dataValue;\n    }\n\n    private T ConvertFromStorageFormat<T>(byte[] value)\n    {\n        T dataValue = default!;\n\n        try\n        {\n            if (value is { Length: > 0 })\n                dataValue = this.serializer.Deserialize<T>(value)!;\n        }\n        catch (Exception exc)\n        {\n            var message = $\"Unable to convert from storage format, Data={value}, DataLen={value?.Length ?? -1}, StateType={typeof(T)}\";\n            LogError(logger, message);","sourceCodeStart":400,"sourceCodeEnd":436,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/AWS/Orleans.Transactions.DynamoDB/TransactionalState/DynamoDBTransactionalStateStorage.cs#L400-L436","documentation":"Thrown by the StateEntity overload of ConvertFromStorageFormat<T> when this.serializer.Deserialize<T>(entity.State) throws while turning a stored StateEntity's binary State back into a TState. The library wraps the deserializer's exception in an AggregateException whose message echoes the raw Data bytes and, if a partial value exists, the partially-deserialized value and its type. A failure here means the bytes in DynamoDB cannot be turned into the requested CLR type by the configured IGrainStorageSerializer.","triggerScenarios":"Produced while recovering pending/prepared transaction states during Load() (each pending StateEntity is deserialized via ConvertFromStorageFormat(StateEntity)). It fires when entity.State is non-empty but the serializer throws: e.g., the TState class changed in an incompatible way, the serializer was changed (e.g., from Newtonsoft.Json to System.Text.Json) without a custom serializer, or the stored bytes are corrupt.","commonSituations":"Deploying a new version of the grain state class whose fields/types no longer match persisted rows; switching GrainStorageSerializer config between deployments; hand-edited or partially-written rows in the table; encryptor/compression interceptor misconfiguration that was changed since the row was written.","solutions":["Identify which StateEntity row is failing from the log (PartitionKey/SequenceId) and inspect its State bytes.","Revert or migrate the TState type / serializer to be backward-compatible with the persisted rows, or implement a custom IGrainStorageSerializer that can read the legacy format.","If the row is definitively corrupt, delete the offending state row(s) for that grain so a fresh state is created (data loss for that grain).","Pin the same GrainStorageSerializer and TState shape across all deployments that share the table."],"exampleFix":"// before: serializer changed between releases\noptions.GrainStorageSerializer = new JsonGrainStorageSerializer();\n\n// after: keep a stable, version-tolerant serializer across releases\noptions.GrainStorageSerializer = new OrleansPersistenceCustomJsonCodec(/* config with version tolerance */);","handlingStrategy":"try-catch","validationCode":"// Before loading, sanity-check that the configured serializer can round-trip your state type\nvar sample = new TState();\nvar bytes = options.GrainStorageSerializer.Serialize(sample).ToArray();\nvar roundTrip = options.GrainStorageSerializer.Deserialize<TState>(bytes);\nif (roundTrip is null) throw new InvalidOperationException(\"Serializer cannot round-trip TState\");","typeGuard":"static bool CanDeserializeState(IGrainStorageSerializer s, byte[] data)\n    where TState : class, new()\n{\n    try { return s.Deserialize<TState>(data) is not null; }\n    catch { return false; }\n}","tryCatchPattern":"try\n{\n    await grain.LoadOrRecover();\n}\ncatch (AggregateException ax) when (ax.InnerException is not null\n    && ax.Message.Contains(\"Unable to convert from storage format\"))\n{\n    _logger.LogCritical(ax.InnerException, \"State deserialization failed; row format is incompatible with TState\");\n    // quarantine the grain or reset its state per your data-loss policy\n    throw;\n}","preventionTips":["Never change the TState shape or serializer without a backward-compatible migration path.","Pin the same IGrainStorageSerializer across all environments that share a table.","Keep grain state types simple and additive; use [JsonExtensionData] (or equivalent) to tolerate new fields.","Store a schema/version tag alongside state so you can detect incompatible rows proactively."],"tags":["dynamodb","transactions","serialization","deserialization","schema-migration"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}