New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unable to Deserialize Data Serialized with Typeless Resolver after Migrating to Attributed Objects #1806
Comments
The only way to ignore the attributes is to explicitly invoke a different formatter. You could maybe do this with a custom resolver, but since the formatter you had been using was also dynamically created by the Seeing the callstack for the exception would help. And knowing which path you took (IsTypeless or !IsTypeless) too. |
for data that was previously serialized with Typeless, it always takes the "IsTypeless" path, that part works correctly.
|
I've created the following test case: const string PathPrefix = "C:\\SomePath";
// [MessagePackObject]
public class TestObject
{
// [Key(0)]
public string Name { get; set; }
// [Key(1)]
public int Age { get; set; }
}
readonly TestObject _testObject = new()
{
Name = "John",
Age = 42
};
[Test]
public void CreateBinary()
{
var bytes = MessagePack.MessagePackSerializer.Typeless.Serialize(_testObject);
File.WriteAllBytes(Path.Join(PathPrefix, "test.bin"), bytes);
}
[Test]
public void DeserializeBinary()
{
var bytes = File.ReadAllBytes(Path.Join(PathPrefix, "test.bin"));
var obj = MessagePack.MessagePackSerializer.Typeless.Deserialize(bytes);
obj.Should().BeEquivalentTo(_testObject);
} If I run both tests they pass, with and without Attributes. But if I run
And since I've added attributes to my models, my idea was to deserialize the stored data in the exact same way as before when "IsTypeless" returned true. |
Thanks for the added detail. As this is a specialized request that will likely take 2-3 hours to investigate, further investigation goes beyond the time I have to offer for free support. |
💵 Thank you for helping to sponsor development of this project. 💵 The problem turns out to have nothing directly to do with typeless. It has to do with contractless. {
"Name": "a",
"Age": 2
} This means your original object that was serialized with typeless+contractless was formatted as a map with named properties. But when you added attributes to your class, you used ordinal keys in the ["a", 2] When you get the "Unexpected msgpack code 130 (fixmap) encountered" error, it's because in deserializing it expected an array but encountered a map instead. So, you can fix compatibility simply by changing the attributes you're adding: [MessagePackObject]
public class TestObject
{
- [Key(0)]
+ [Key("Name")]
public string Name { get; set; }
- [Key(1)]
+ [Key("Age")]
public int Age { get; set; }
} By adding these attributes, and continuing to use the Typeless front-end, MessagePackSerializer will correctly parse the typeless header (with the full name, which hasn't changed), and when it finds the So I think that should get you going. |
And BTW, I wrote this test that reproduces the original failure without having to write to a file, recompile the test and rerun: [MessagePackObject]
public class Issue1806Map
{
[Key("Name")]
public string Name { get; set; }
[Key("Age")]
public int Age { get; set; }
}
[MessagePackObject]
public class Issue1806Array
{
[Key(0)]
public string Name { get; set; }
[Key(1)]
public int Age { get; set; }
}
[Fact]
public void DeserializeTypelessWithAttributedTypes()
{
var mapType = new Issue1806Map { Name = "a", Age = 2 };
byte[] bytes = MessagePackSerializer.Serialize(mapType);
this.logger.WriteLine(Convert.ToBase64String(bytes));
Issue1806Array arrayType = MessagePackSerializer.Deserialize<Issue1806Array>(bytes);
} So theoretically if you wanted a way to use arrays when serializing but support deserializing both arrays and maps, this code could help you easily test progress toward your goal. |
Thanks for looking into this issue. Avoiding keys for names was the goal due to size/speed concerns. Do you think creating a custom formatter for this behavior would be complex? |
Ideally this behavior would be enabled in the |
I agree that would make it super easy to access. But this is a formatter-level incompatibility, and the Options are serializer-wide. What you really need is a custom formatter that will detect whether a map or an array header is the next to be read, and invoke the appropriate formatter based on that. But the formatters you're using (both before and after) are both dynamically created at runtime and aren't directly accessible. Their resolvers are, and you might be able to detect and delegate to them, but only if the contractless resolver isn't "smart enough" to see that the type is attributed and reject it or forward it on to the other resolver. If the contractless resolver won't accept an attributed type, you'd have to code up the formatter yourself. You could do this manually, or you could use our mpc tool (or the newer, unreleased source generator) to do it for you and then check the generated code in as your own. Another option that doesn't require any special MessagePack related code is to have two sets of data types. One is on ice, and uses the maps schema. You'll never change it again. Your other set of data types will use the array schema, and each have a copy constructor that accepts the older data type and copies from it. This way, you can evolve your data type (the one that uses arrays) all you want, and you'll always be able to deserialize the map-based schema so long as you keep the old types around and keep your copy constructors current. |
Would the effort for this solution be significant? Eventually other people will run into this limitation, so it would make sense to be a "feature". The last option, would be the easiest in theory but it would require duplicating a large number of data models. |
It looks like you can bypass the check for attributes and specify the [Fact]
public void DeserializeTypelessWithAttributedTypes()
{
var mapType = new Issue1806Map { Name = "a", Age = 2 };
byte[] bytes = MessagePackSerializer.Serialize(mapType);
this.logger.WriteLine(Convert.ToBase64String(bytes));
Issue1806Array arrayType = MessagePackSerializer.Deserialize<Issue1806Array>(
bytes,
MessagePackSerializerOptions.Standard.WithResolver(DynamicContractlessObjectResolver.Instance));
} Notice how we're deserializing into the array-schema'd type, but using contractless anyway, which lets us deserialize a map instead of an array. |
I managed to get your unit test working, by using the following resolver: readonly MessagePackSerializerOptions _msgPackOptions = MessagePackSerializerOptions.Standard.WithResolver(
CompositeResolver.Create([
NativeGuidFormatter.Instance,
NativeDateTimeFormatter.Instance,
], [
NativeDateTimeResolver.Instance,
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
DynamicContractlessObjectResolverAllowPrivate.Instance
])); However, the unit test you proposed has both classes with Do you think a solution for this is still feasible? |
Yes, I still think it's feasible. Your attempt is along the right lines as far as chaining resolvers go. |
Following is a fully working sample that meets your requirements, as I understand them. // The important thing about this resolver is that the
// custom ContractlessOrAttributedResolver appears instead of (or before) the
// DynamicGenericResolver, DynamicObjectResolver or DynamicContractlessObjectResolver resolvers.
static readonly IFormatterResolver CustomResolver = CompositeResolver.Create(
new[]
{
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
ImmutableCollectionResolver.Instance,
CompositeResolver.Create(ExpandoObjectFormatter.Instance),
ContractlessOrAttributedResolver.Instance,
TypelessObjectResolver.Instance,
});
// This represents all your data types, which should have attributes on them
// and should use ordinals rather than strings for their Key attribute arguments.
[MessagePackObject]
public class Issue1806Array
{
[Key(0)]
public string Name { get; set; }
[Key(1)]
public int Age { get; set; }
}
[Fact]
public void DeserializeTypelessWithAttributedTypes()
{
MessagePackSerializerOptions options = new TypeSubstitutingOptions(CustomResolver);
// Serialize in the old way, with maps and the Typeless annotation.
var mapType = new Issue1806Map { Name = "a", Age = 2 };
byte[] bytes = MessagePackSerializer.Typeless.Serialize(mapType);
this.logger.WriteLine(Convert.ToBase64String(bytes));
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bytes));
// Deserialize using the new type, and verify the result.
Issue1806Array arrayType = MessagePackSerializer.Deserialize<Issue1806Array>(bytes, options);
Assert.Equal(mapType.Name, arrayType.Name);
Assert.Equal(mapType.Age, arrayType.Age);
// Serialize using the new format, and verify that it's a shorter msgpack representation
// since it's lacking typeless annotations and uses arrays instead of maps.
byte[] bytes2 = MessagePackSerializer.Serialize(arrayType);
this.logger.WriteLine(Convert.ToBase64String(bytes2));
this.logger.WriteLine(MessagePackSerializer.ConvertToJson(bytes2));
Assert.True(bytes2.Length < bytes.Length);
// Finally, demonstrate deserialization of the new format works as expected.
Issue1806Array arrayType2 = MessagePackSerializer.Deserialize<Issue1806Array>(bytes2, options);
Assert.Equal(arrayType.Name, arrayType2.Name);
Assert.Equal(arrayType.Age, arrayType2.Age);
}
// This is where most of the magic is.
class ContractlessOrAttributedResolver : IFormatterResolver
{
internal static readonly ContractlessOrAttributedResolver Instance = new();
private ContractlessOrAttributedResolver() { }
public IMessagePackFormatter<T> GetFormatter<T>()
{
// If this activates the formatter too much, we can filter here and return null.
// For example, if our custom formatter were activated for Int32, we could avoid that like this:
if (typeof(T) == typeof(int))
{
return null;
}
return ContractlessOrAttributedFormatter<T>.Instance;
}
class ContractlessOrAttributedFormatter<T> : IMessagePackFormatter<T>
{
internal static readonly ContractlessOrAttributedFormatter<T> Instance = new();
private static readonly IMessagePackFormatter<T> AttributedFormatter = DynamicGenericResolver.Instance.GetFormatter<T>() ?? DynamicObjectResolver.Instance.GetFormatterWithVerify<T>();
private static readonly IMessagePackFormatter<T> ContractlessFormatter = DynamicContractlessObjectResolver.Instance.GetFormatterWithVerify<T>();
private ContractlessOrAttributedFormatter() { }
public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
if (IsTypeless(reader))
{
return (T)TypelessFormatter.Instance.Deserialize(ref reader, options);
}
IMessagePackFormatter<T> formatter = reader.NextMessagePackType switch
{
MessagePackType.Array => AttributedFormatter,
MessagePackType.Map => ContractlessFormatter,
_ => throw new MessagePackSerializationException("Unexpected msgpack code: " + reader.NextMessagePackType),
};
return formatter.Deserialize(ref reader, options);
}
public void Serialize(ref MessagePackWriter writer, T value, MessagePackSerializerOptions options)
{
// Always serialize using the attributed formatter.
AttributedFormatter.Serialize(ref writer, value, options);
}
private static bool IsTypeless(MessagePackReader reader) => reader.NextMessagePackType == MessagePackType.Extension && reader.ReadExtensionFormatHeader().TypeCode == 100;
}
}
// The rest of the code is only important for the test, which is specially contrived
// to use *two* classes (one attributed, one not) instead of just one attributed class.
// In your own product code, omit all the code below,
// and anywhere that `Issue1806Map` had been referenced above,
// change to `Issue1806Array` (or actually, your own attributed data types).
class TypeSubstitutingOptions : MessagePackSerializerOptions
{
public TypeSubstitutingOptions(IFormatterResolver resolver)
: base(resolver)
{
}
protected TypeSubstitutingOptions(MessagePackSerializerOptions copyFrom)
: base(copyFrom)
{
}
public override Type LoadType(string typeName)
{
if (typeName == typeof(Issue1806Map).AssemblyQualifiedName)
{
return typeof(Issue1806Array);
}
return base.LoadType(typeName);
}
protected override MessagePackSerializerOptions Clone() => new TypeSubstitutingOptions(this);
}
public class Issue1806Map
{
public string Name { get; set; }
public int Age { get; set; }
} |
The output of the above test, BTW, is:
The first two lines are your 'before' schema and the second two lines are your 'after' schema, demonstrating the changes you want have taken place, and that both formats can be read, but only the newer one written by the serializer. |
Thank you so much. I've tested your solution and it works. |
Description:
Previously, I was serializing all my objects using the
TypelessContractlessStandardResolver
resolver. However, I am looking to migrate all objects to attributed. I've added the attributes[MessagePackObject]
and[Key(..)]
to all the classes.Everything works fine, except for the data serialized with typeless that now needs to be deserialized. To check how the data was serialized, I have the following method:
If it returns true, I try to deserialize it with
MessagePack.MessagePackSerializer.Typeless.Deserialize
. However, I always getUnexpected msgpack code 133 (fixmap) encountered
. I suspect that this is happening because now the classes are annotated, whereas at the time of the serialization, the classes didn't have any attributes. If I remove the annotations, the deserialization works.Question:
Is there any way to simply ignore the attributes so I can deserialize the old data?
The text was updated successfully, but these errors were encountered: