New! Benchmarks graphs are now available to better visualize the performance of ServiceStack’s JSON and JSV text serializers.
As there have been a few people trying to use TypeSerializer in dynamic situations, I thought I’d put together a post detailing some restrictions and highlighting the kind of use-case scenarios that is possible with TypeSerializer and its JSV format.
Some of the goals for the JSV format was to be both compact in size and resilient to versioning and schema changes. With these goals in mind, a conscience design decision was made to not include any type information with the serialized payload. So the way that JSV does its de-serializing is by coercing the JSV payload into the type specified by the user. This can be seen in the API provided by the TypeSerializer class allowing the client to deserialize based on a runtime or static generic type:
T DeserializeFromString<T>(string value)
object DeserializeFromString(string value, Type type)
The consequences of this means the user in one way or another must supply the type information although at the same time it allows the same JSV payload to be deserialized back into different types. For example every POCO type can be deserialized back into a Dictionary<string,string> which is useful when you want to still access the data but for whatever reason do not have the type that created it. This also allows for some interesting versioning possibilities in which the format can withstand large changes in its schemas as seen in the article Painless data migrations with schema-less NoSQL datastores and Redis.
Beyond normal serialization of DTO types, TypeSerializer is also able to serialize deep heirachys and Interface types as well as ‘late-bound objects’. The problem with trying to deserialize a late-bound object (i.e. a property with an object type) is that TypeSerializer doesn’t know what type to de-serialize it back into – and since a string is a valid object, will simply populate the object property with the string contents of the serialized property value.
With this in mind, the best way to deserialize a POCO type with a dynamic object property is to serialize the Type information yourself along with the payload. Of course it is best to highlight what this means with an example.
The example below shows how you can serialize a message with a dynamic object payload and have it deserialize back into a DynamicMessage as well as alternate GenericMessage and a StrictMessage types sharing a similar definition – all as expected, without any data loss.
public class DynamicMessage : IMessageHeaders
{
public Guid Id { get; set; }
public string ReplyTo { get; set; }
public int Priority { get; set; }
public int RetryAttempts { get; set; }
public object Body { get; set; }
public Type Type { get; set; }
public object GetBody()
{
//When deserialized this.Body is a string so use the serilaized
//this.Type to deserialize it back into the original type
return this.Body is string
? TypeSerializer.DeserializeFromString((string)this.Body, this.Type)
: this.Body;
}
}
public class GenericMessage<T> : IMessageHeaders
{
public Guid Id { get; set; }
public string ReplyTo { get; set; }
public int Priority { get; set; }
public int RetryAttempts { get; set; }
public T Body { get; set; }
}
public class StrictMessage : IMessageHeaders
{
public Guid Id { get; set; }
public string ReplyTo { get; set; }
public int Priority { get; set; }
public int RetryAttempts { get; set; }
public MessageBody Body { get; set; }
}
public class MessageBody
{
public MessageBody()
{
this.Arguments = new List<string>();
}
public string Action { get; set; }
public List<string> Arguments { get; set; }
}
/// Common interface not required, used only to simplify validation
public interface IMessageHeaders
{
Guid Id { get; set; }
string ReplyTo { get; set; }
int Priority { get; set; }
int RetryAttempts { get; set; }
}
[TestFixture]
public class DynamicMessageTests
{
[Test]
public void Can_deserialize_between_dynamic_generic_and_strict_messages()
{
var original = new DynamicMessage
{
Id = Guid.NewGuid(),
Priority = 3,
ReplyTo = "http://path/to/reply.svc",
RetryAttempts = 1,
Type = typeof(MessageBody),
Body = new MessageBody
{
Action = "Alphabet",
Arguments = { "a", "b", "c" }
}
};
var jsv = TypeSerializer.SerializeToString(original);
var dynamicType = TypeSerializer.DeserializeFromString<DynamicMessage>(jsv);
var genericType = TypeSerializer.DeserializeFromString<GenericMessage<MessageBody>>(jsv);
var strictType = TypeSerializer.DeserializeFromString<StrictMessage>(jsv);
AssertHeadersAreEqual(dynamicType, original);
AssertBodyIsEqual(dynamicType.GetBody(), (MessageBody)original.Body);
AssertHeadersAreEqual(genericType, original);
AssertBodyIsEqual(genericType.Body, (MessageBody)original.Body);
AssertHeadersAreEqual(strictType, original);
AssertBodyIsEqual(strictType.Body, (MessageBody)original.Body);
//Using T.Dump() ext method to view output
Console.WriteLine(strictType.Dump());
/* Output:
{
Id: 891653ea2d0a4626ab0623fc2dc9dce1,
ReplyTo: http://path/to/reply.svc,
Priority: 3,
RetryAttempts: 1,
Body:
{
Action: Alphabet,
Arguments:
[
a,
b,
c
]
}
}
*/
}
public void AssertHeadersAreEqual(IMessageHeaders actual, IMessageHeaders expected)
{
Assert.That(actual.Id, Is.EqualTo(expected.Id));
Assert.That(actual.ReplyTo, Is.EqualTo(expected.ReplyTo));
Assert.That(actual.Priority, Is.EqualTo(expected.Priority));
Assert.That(actual.RetryAttempts, Is.EqualTo(expected.RetryAttempts));
}
public void AssertBodyIsEqual(object actual, MessageBody expected)
{
var actualBody = actual as MessageBody;
Assert.That(actualBody, Is.Not.Null);
Assert.That(actualBody.Action, Is.EqualTo(expected.Action));
Assert.That(actualBody.Arguments, Is.EquivalentTo(expected.Arguments));
}
}
The source of this runnable example can be found as part of TypeSerializer’s test suite in the DynamicMessageTests.cs test class. Some more dynamic examples showing advanced usages of TypeSerializer can be found in the ComplexObjectGraphTest.cs class within the same directory.