4

Serialization and deserialization in .NET applications can be achieved using several libraries, e.g.

  • Newtonsoft.Json
  • System.Text.Json

Assuming I'm developing a new application where I want to use Newtonsoft.Json but I want to keep the flexibility to switch to System.Text.Json (or another library) at a later point in time (e.g. in the hypothetical scenario that System.Text.Json doesn't currently support certain features I need but it will in future):

Would it make sense to work with an interface and inject it wherever I need to serialize / deserialize something, so that once I want to switch the library, I can simply implement a new class and resolve the dependency accordingly at a centralized point of my application?

Sample code:

Interface:

public interface ISerializer
{
    string SerializeObject<T>(T obj);
    T DeserializeObject<T>(string str);
}

Implementation with Newtonsoft.Json

public class NewtonsoftJsonSerializer : ISerializer
{
    public string SerializeObject<T>(T obj)
    {
        try
        {
            return JsonConvert.SerializeObject(obj);
        }
        catch (JsonException e)
        {
            throw new InvalidOperationException("Serialization failed.", e);
        }
    }

    public T DeserializeObject<T>(string str)
    {
        try
        {
            return JsonConvert.DeserializeObject<T>(str);
        }
        catch (JsonException e)
        {
            throw new InvalidOperationException("Deserialization failed.", e);
        }
    }
}

Implementation with System.Text.Json

public class SystemTextJsonSerializer : ISerializer
{
    public string SerializeObject<T>(T obj)
    {
        try
        {
            return JsonSerializer.Serialize(obj);
        }
        catch (JsonException e)
        {
            throw new InvalidOperationException("Serialization failed.", e);
        }
    }

    public T DeserializeObject<T>(string str)
    {
        try
        {
            return JsonSerializer.Deserialize<T>(str);
        }
        catch (JsonException e)
        {
            throw new InvalidOperationException("Deserialization failed.", e);
        }
    }
}

I realize that this may seem a bit fussy, but it seems like a good idea to replace the implementation in a simple way in a single place (in the dependency resolution) without having to replace namespaces and method calls in many classes.

This procedure could also be useful if, for example, you want to switch from XML serialization to JSON serialization.

4
  • 4
    Serialization is typically more framework-like than library-like, which makes it difficult to abstract over. Things are fairly good in the .NET world because these serialization mechanisms can use reflection, but you're locked in to one solution as soon as you start using [Attributes] that only one of them supports. This isn't necessarily a problem. You don't have to make everything perfectly dynamic and flexible, because if your requirements change in the future then you can still edit your code (as long as you control deployment and all dependents).
    – amon
    Commented Jan 8 at 16:03
  • 1
    In the Rust world, serde is a pretty successful framework targeting different serialization methods. Commented Jan 8 at 16:05
  • 1
    @Hans-MartinMosner serde is nice because it provides a single, shared set of attributes. Abstracting over different existing libraries that use different attributes is not as pretty. Commented Jan 9 at 8:40
  • @SebastianRedl true, but the variety of serde implementations (including gasp CSV) indicates that they're doing something right. Commented Jan 9 at 8:44

3 Answers 3

11

Abstraction makes sense when you either

  • have an existing collection of concrete classes or types calling for an abstract concept (be it interface, trait, or abstract superclass), or
  • write a framework which potentially supports multiple concrete classes even if you currently have only one.

In your case, you seem to want to write a framework, an urge that you should resist unless you are certain that this is really what you need. Premature "frameworkization" is as dangerous as premature optimization - you need sufficient experience and data to decide which road to follow. Just imagining a future problem that might be solved is often not a good enough reason.

Note that there are many cases of excellent frameworks in many programming language ecosystems, but I'm pretty certain most of those are the result of multiple evolutionary steps starting at a pretty specialized solution and finding the need to generalize and abstract later to cover a bigger range.

1

It makes sense when you have multiple ways your want to serialise your objects, say json, csv, binary etc but I wouldn't both for just future refactoring.

You want to avoid empty layers in your code. So if you have already wrapped your de/serilisation in a repository or client layer eg

public class Repo : IRepo
{
  void Save(ob)
 {
   File.Write(this.json.Serialse(obj, this.options))
 }
}

There is a downside to adding more empty wrapping code:

public class Repo : IRepo
{
  IFileWriter file;
  IJson json;
  void Save(ob)
 {
   this.file.Write(this.json.Serialse(obj, this.options))
 }
}

public class Json
{
  string Serialise(obj) { this.json.Serialise(....
}

public class File
{
   void Save(obj) { this.file.save(....
}

You have more code to write bugs in, step through, search, and even if new libraries come along if they break the interface. ie

NewFluentJson.OnlyHasFluentSerialise().CallThisSetupFirst(colour).WithObject(o=> obj);

Then you made no saving in refactoring.

1

I did this once upon a time: https://github.com/BrannonKing/CommonSerializer . It's not a lot of code. There was Nuget packages for it too, but I don't think anyone ever used it.

It was for a project I was working on where I wanted the same serializer interface for things I was about to send over the wire vs things going to disk (e.g. binary vs text). Sometimes it was really handy to be able to debug the network traffic by visual inspection!

I was also doing a lot of speed comparisons between the various serializers at the time. I think many of the serializers use static type caching (which you are implicitly relying on when using static serializer calls), but I'm not sure that they all do. It may be useful to have a single instance of some serializer, either for type caching or for standardizing options. I thought about making local storage format be a user option (although that sounds like a terrible idea in retrospect), or even supporting user-supplied database providers.

Another thing that was handy with the interface: the various serializers had differing mechanisms for supporting type inheritance. Putting that into the interface enabled easier testing of that feature. It's worth looking at my code for that utility.

I had wanted all the serializers to support .NET's DataContract/Member attributes, since that seemed like the most universal and easily accessible attributes at the time. I don't remember if they all did, in the end, support that or not. I know that I had to use DataMember.Order for Protobuf support.

Another handy pattern that I used was this backwards-compatibility approach: instead of versioning DTOs, I would mark legacy fields like this:

[DataMember(name='Name', EmitDefaultValue=false, IsRequired=false), Obsolete]
internal MyDto LegacyName { set {...} }

I don't remember which serializers supported setter-only properties, but I think that was a reasonably common feature.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.