Hello World
Spiga

JsonMe - 合约与类型分离的轻量级JSON映射类库

2010-10-11 00:29 by 老赵, 4896 visits

JSON全称为JavaScript Object Notation,原本作为JavaScript语言中用于表示对象结构的文本形式。不过目前JSON成功地脱离了JavaScript语言,它已经成为一种运用十分广泛的数据交换格式。从表面看来,目前用于某个对象与JSON格式之间相互转化的解决方案已经有了许多种,例如在.NET平台上,我们可以使用ASP.NET AJAX中引入的JavaScriptSerializer,WCF中引入的DataContractJsonSerializer,亦或是Json.NET。但是,最近我忽然发现这些类库都无法满足我的要求,因此,我今天花了一点时间,写了一个非常简单的对象与JSON格式相互转化的类库,是为JsonMe

现有解决方案的不足

您可能会感到疑惑,难道现有的解决方案都不够好吗?又搞一个新的实现出来,这不是重复造轮子嘛。但事实上,它们在我眼里,都有一些难以逾越的障碍。就拿JavaScriptSerializer来说,它使用起来十分简单,配合C#的匿名对象特性,输出一个JSON格式可谓无比直接:

var value = new
{
    hello = "world",
    array = new object[] { 1, 2, 3, "jeffz" }
};

var json = new JavaScriptSerializer().Serialize(value);

JavaScriptSerializer也支持JSON格式与某种类型的对象相互转化,甚至可以加上ScriptIgnoreAttribute标记来忽略某个属性,例如:

public class Post
{
    public string Title { get; set; }

    [ScriptIgnore]
    public string Content { get; set; }

    public DateTime CreateTime { get; set; }
}

如果我想要改变某个属性在JSON中的字段名呢?JavaScriptSerializer应该也做得到,但我现在我一时想不起来了,也懒得去查。我知道DataContractJsonSerializer一定支持,事实上它是.NET中用于代替JavaScriptSerializer的JSON序列化解决方案,为此如今的JavaScriptSerializer已经标记了ObsolateAttribute,编译时您应该可以看到warning。

那么我再提一个要求:在序列化某个字段的时候,对它的值进行一个简单的转化。例如,我希望将DateTime对象在JSON中表现为字符串,这对于JavaScriptSerializer和DataContractJsonSerializer来说似乎就做不到了,至少会十分麻烦。但是这对于Json.NET来说不是个问题,相比于前两者来说,Json.NET可谓是JSON解决方案中的Word和Excel,经过了许多版本的积极开发,如今的Json.NET已经实现了大量的功能,几乎在每一处都提供了扩展点。我对Json.NET的了解不多,不过在简单浏览代码以后,我发现它的复杂度已经能和如今的ASP.NET MVC比肩了,是不是显得有些可怕?我也这么觉得。

但事实上,让我重新写一个JsonMe的关键,是因为我希望同一种类型能够与不同的JSON格式相互转化。试想,我有一个User类型,但是我却有两个不同格式的JSON数据源。或者说,相同的User对象,需要根据环境得到两种不同格式的JSON输出。这样JavaScriptSerializer或DataContractJsonSerializer就无法满足我的要求了,因为它们使用自定义特性来控制JSON的格式,但一个类型只能应用一种JSON策略,这又让我如何是好?

当然,这点对于Json.NET应该不是问题,但是它实在太复杂了,如果我能把它研究透彻并加以扩展,还不如自己重新去写一个简单的类库呢。于是,一个秋高气爽的周日下午,JsonMe就此诞生了。

JsonMe使用方式

为了实现以上的要求,在JsonMe中我将JSON转化的“合约”与类型本身分离。我很喜欢这样的策略,正如EasyMongo那样,除了一个类型可以对应多种映射策略之外,我还能将一种类型与它的映射策略解耦。例如,现在有一个User对象:

public class User
{
    public string UserName { get; set; }
    public int Age { get; set; }
    public bool IsAdult { get { return this.Age >= 18; } }
}

在进行序列化之前,需要先定义这样的“合约”(即映射策略):

var userContract = new JsonContract<User>();
userContract.SimpleProperty(u => u.UserName).Name("Name");
userContract.SimpleProperty(u => u.Age);

User类型中定义了三个属性,其中我们只采纳了两个:Age和UserName(并改名为Name)。利用这个“合约”我们可以进行JSON转化:

var user = new User { UserName = "Tom", Age = 20 };
var jsonUser = JsonSerializer.SerializeObject(user, userContract);
Console.WriteLine(jsonUser);

这将会输出(已经过手动格式化):

{
    "Name" : "Tom",
    "Age" : 20
}

同样,我们可以为某个属性运用转化规则,只要提供一个IJsonConverter对象即可:

public class Post
{
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime CreateTime { get; set; }
}

public class DataTimeConverter : IJsonConverter
{
    public object ToJsonValue(Type type, object value)
    {
        return ((DateTime)value).ToString("R");
    }

    public object FromJsonValue(Type type, object value)
    {
        return DateTime.ParseExact((string)value, "R", null);
    }
}

定义合约:

var postContract = new JsonContract<Post>();
postContract.SimpleProperty(p => p.Title);
postContract.SimpleProperty(p => p.CreateTime).Converter(new DataTimeConverter());

在这里,DataTimeConverter会将CreateTime属性的时间日期转化为字符串以后再输出。例如:

var post = new Post { Title = "Good day today.", CreateTime = DateTime.Now };
Console.WriteLine(JsonSerializer.SerializeObject(post, postContract));
Console.WriteLine();

便会得到:

{
    "Title" : "Good day today.",
    "CreateTime" : "Sun, 10 Oct 2010 23:12:53 GMT"
}

我们可以做得还有更多,例如这里有一个包含复杂属性的Category对象:

public class Category
{
    public string Name { get; set; }
    public User Author { get; set; }
    public List<Post> Posts { get; set; }
}

我们在定义Category的合约时,则可以用到之前的合约:

var categoryContract = new JsonContract<Category>();
categoryContract.SimpleProperty(p => p.Name);
categoryContract.ComplexProperty(p => p.Author).Contract(userContract);
categoryContract.ArrayProperty(p => p.Posts).ElementContract(postContract);

那么下面这段代码:

var category = new Category
{
    Name = "Default",
    Author = new User { UserName = "Jerry", Age = 15 },
    Posts = new List<Post>
    {
        new Post { Title = "Post 1", CreateTime = new DateTime(2010, 1, 1) },
        new Post { Title = "Post 2", CreateTime = new DateTime(2010, 2, 1) },
        new Post { Title = "Post 3", CreateTime = new DateTime(2010, 3, 1) }
    }
};
var jsonCategory = JsonSerializer.SerializeObject(category, s_categoryContract);
Console.WriteLine(jsonCategory);

将会输出:

{
    "Name" : "Default",
    "Author" :
    {
        "Name" : "Jerry",
        "Age" : 15
    },
    "Posts" :
    [
        {
            "Title" : "Post 1",
            "CreateTime" : "Fri, 01 Jan 2010 00:00:00 GMT"
        },
        {
            "Title" : "Post 2",
            "CreateTime" : "Mon, 01 Feb 2010 00:00:00 GMT"
        },
        {
            "Title" : "Post 3",
            "CreateTime" : "Mon, 01 Mar 2010 00:00:00 GMT"
        }
    ]
}

当然,您同样可以定义一个匿名对象作为JSON输出:

var value = new { v = JsonSerializer.SerializeObject(category, categoryContract) };
Console.WriteLine(JsonSerializer.Serialize(value));

是不是很简单?

更多讨论

以上代码只是演示了序列化成JSON的功能,但是您应该也可以了解反序列化的使用方式。目前JsonMe的功能就只有这么多,可以说非常简单,但是我认为已经基本够用,甚至在大部分情况下完整代替JavaScriptSerializer和DataContractJsonSerialzier是没有什么问题的。

开发JsonMe恰好花了我五个小时(下午2点到7点),只有几百行代码(当然还很粗糙),大部分还是用于合约配置的“骨架”,真正进行对象属性的赋值和转化的代码只有一百多行。JsonMe能够如此简约的原因,是因为站在JavaScriptSerializer的肩膀上。说实话,JavaScriptSerializer其实提供了一个很好的基础,因为它可以将一段JSON字符串转化为Dictionary<string, object>与object数组间的嵌套,这正是JSON格式的本质。当然,为了实现简单,我在JsonMe中创建了JsonObject和JsonArray两个对象,分别继承Dictionary<string, dynamic>和List<dynamic>,它们便作为JSON结构的表现形式。

不过我也意识到,JavaScriptSerializer可能并不是一个合适的选择,因为这会让我们依赖System.Web.Extensions.dll。事实上在.NET平台上有一个更独立,更简单的JSON结构实现,那就是Silverlight中的System.Json.dll。只可惜我们只能用它开发Silverlight程序。我打算在合适的时候,将mono中的System.Json.dll实现移植到.NET 3.5中,这样JsonMe就可以摆脱对System.Web.Extensions.dll的依赖,并摆脱自定义的JsonObject和JsonArray,可以直接使用System.Json里的结构(已完成)。更重要的是,这可以让JsonMe作用在Silverlight,甚至是基于MonoTouch的iOS开发中(很有可能还包括未来的MonoDroid)。

如果您感兴趣的话,也不妨获取JsonMe的源代码和简单示例,修改修改,尝试尝试。我认为它还是相当实用的。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

18 条回复

  1. jun1st
    222.73.22.*
    链接

    jun1st 2010-10-11 10:05:49

    这个有劲 不过发现个错别字“我打算在合时的时候”

  2. 老赵
    admin
    链接

    老赵 2010-10-11 10:42:33

    @jun1st

    谢谢,改好了。

  3. 1-2-3
    119.119.217.*
    链接

    1-2-3 2010-10-11 15:59:57

    是不是每个简单属性都要这么注册一下呀?

    categoryContract.SimpleProperty(p => p.Name);
    

    好麻烦的说。最好能这样:普通字符串是一类,普通数字是一类,普通日期是一类,缺省使用公共设置,特殊情况特殊注册。引用对象特殊注册(为防止循环引用)

  4. 老赵
    admin
    链接

    老赵 2010-10-11 17:48:51

    @1-2-3

    在我看来一个协议是不能轻易受到影响的,因此我不希望一个对象增加了一个属性之后,会自动改变JSON输出内容,所以我要求一切字段都要显式地标记才行……

  5. 1-2-3
    119.119.217.*
    链接

    1-2-3 2010-10-12 11:40:44

    @老赵

    哦,原来并不是一个 JsonConverter,而是作为一个协议。但是协议和Converter不要实现在一起比较好吧?万一哪天又想换成 XML 格式的呢?是不是应该有一个协议,然后有一个把协议转换成 Json 格式的 Converter 比较好?这样以后还可以再实现 XMLConverter。

    另,我们项目是基于 Json.Net,自定义它的 ContractResolver 实现,也是很方便很灵活的。

  6. 老赵
    admin
    链接

    老赵 2010-10-12 14:40:57

    @123

    XML序列化的问题以后再说吧,虽然我也想这么做,但是感觉很难设计,就像.NET里的System.Runtime.Serialization设计的就复杂了。

    我看过Json.NET的ContractResolver,我觉得要么是不够灵活,实现我要的需求会比较麻烦。例如:

    public class Post
    {
        User Author;
        List<User> Coauthors;
    }
    

    我想要在序列化Post的时候,Author和Coauthors里的User对象使用不同的Contract,这就不太容易了吧?

  7. 1-2-3
    119.118.208.*
    链接

    1-2-3 2010-10-12 16:57:02

    @老赵: 我想要在序列化Post的时候,Author和Coauthors里的User对象使用不同的Contract,这就不太容易了吧?

    那倒是,不过这个也太变态了吧?真有这样的需求吗?灵活性总有限度的,过分灵活几乎肯定意味着使用不便,所以得权衡下是不是划算呐。

  8. 老赵
    admin
    链接

    老赵 2010-10-12 17:44:01

    @1-2-3

    我的项目里就有这个需求,所以我实现了JsonMe啊。在JsonMe里可以这么做:

    var contract = new JsonContract<Post>;
    contract.ComplexProperty(p => p.Author).Contract(fullContract);
    contract.ArrayProperty(p => p.Coauthor).Contract(partialContract);
    

    事实上,JsonMe虽然灵活,但是并不复杂,我只写了一个下午,而且已经可以满足我以前所有的JSON方面的需求了。反倒是Json.NET……复杂得让我感到不是滋味,看了它的文档以后,我发现以前也从来没用过Json.NET提供的那么多功能,也想不到一个JSON类库原来可以有这么多功能,呵呵。

  9. 链接

    simon 2010-10-13 09:37:37

    所以我要求一切字段都要显式地标记才行

    当我使用JavaScriptSerializer时,对于大多数情况,我使用默认的方式就已足够。但如果要我全部字段都显式标记,当大量使用起来时,确实有点痛苦。

    我想要在序列化Post的时候,Author和Coauthors里的User对象使用不同的Contract

    我也觉得这个需求有点特殊。

  10. 链接

    陈梓瀚(vczh) 2010-10-15 13:01:13

    所以你们可以去写一个helper class,基于JsonMe和反射来实现自动构造UserContract……

  11. 老赵
    admin
    链接

    老赵 2010-10-15 14:45:58

    @陈梓瀚(vczh)

    如果真需要的确可以这么做,其实无比简单……

  12. syuko
    61.183.207.*
    链接

    syuko 2010-10-21 15:34:24

    每个类库都有促使其产生的原因,只有充分了解了自己的需求后才能决定要不要采用,单从技术的角度出发就决定采用或不采用是不明智的。例如老赵的需求里面就有“在序列化Post的时候,Author和Coauthors里的User对象使用不同的Contract”这种变态的要求,如果我们的需求里面不会出现(当然也不是绝对不会出现)这要的要求,我们就可以使用最简单的DataContractJsonSerializer操作来默认直接序列化,反而简便。

    当然老赵的这个解决方案确实简洁但功能非常强大,代码写的也非常出色,自己花了一天的时间看了一遍(真是汗颜呀)后越发觉得代码写的没有一点可以省去的东西,真是简洁到极致了。代码里面的“ToJson”函数我觉得设计的太好了,还有“JsonPrimitiveProvider”类,“Dictionary s_primitiveProviders”字典....。

    当初我由于自己的需求非常简单就想将“合约”的定制方面给取消,简化一下供自己使用,后来发现简化后简直就把一只公鸡拿走血肉就只剩一地鸡毛了一样,简直就是去其精华。最后还是选择了DataContractJsonSerializer作为序列化和反序列化的工具,不过装箱和拆箱总也避免不了。

  13. 老赵
    admin
    链接

    老赵 2010-10-22 10:01:08

    @syuko

    例如老赵的需求里面就有“在序列化Post的时候,Author和Coauthors里的User对象使用不同的Contract”这种变态的要求

    也不仅仅是,主要是我的确有需求让同一个对象在两个不同的地方使用两种不同的Contract生成JSON。

    当然老赵的这个解决方案确实简洁但功能非常强大

    要不是基于System.Json,还可以写的更简单……不过我觉得改到System.Json以后变得有些乱,有机会一定要整改一下。

  14. water
    222.222.204.*
    链接

    water 2010-10-29 23:03:42

    不知道这个的效率怎么样?

    对比了几个json库的转换效率,感觉都不怎么样。

    我想找一个序列化和反序列化性能比较高的方式。

  15. 老赵
    admin
    链接

    老赵 2010-10-30 17:30:28

    @water

    很显然我这个是不对效率进行优化的,我根本不做实际的序列化反序列化操作啊。

  16. laolaowhn
    122.96.116.*
    链接

    laolaowhn 2011-11-03 12:00:21

    序列化的时候是个递归,反序列化的时候就只有两层,超过3层就没法反序列化了

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Json;
    using JsonMe;
    using System.Collections;
    
    namespace SimpleConsole
    {
        public class User
        {
            public string UserName { get; set; }
            public int Age { get; set; }
            public bool IsAdult { get { return this.Age >= 18; } }
        }
        public class Users : List<User>, IInheritList
        {
            public Object GetObjectFromList(Object obj)
            {
                Users PostsYemp = new Users();
                if (obj is IList)
                {
                    var objList = (IList)obj;
                    foreach (var item in objList)
                    {
                        if (item is User)
                        {
                            PostsYemp.Add((User)item);
                        }
                    }
                }
                return PostsYemp;
            }
        }
        public class Post
        {
            public string Title { get; set; }
            public string Content { get; set; }
            public DateTime CreateTime { get; set; }
            public Users CurrentUsers { get; set; }
    
        }
        public class Posts : List<Post>, IInheritList
        {
            public Object GetObjectFromList(Object obj)
            {
                Posts PostsYemp = new Posts();
                if (obj is IList)
                {
                    var objList = (IList)obj;
                    foreach (var item in objList)
                    {
                        if (item is Post)
                        {
                            PostsYemp.Add((Post)item);
                        }
                    }
                }
                return PostsYemp;
            }
        }
        public class Category
        {
            public string Name { get; set; }
            public User Author { get; set; }
            public List<Post> Posts { get; set; }
            public Posts CurrentPosts { get; set; }
        }
    
        public class DataTimeConverter : IJsonConverter
        {
            public JsonValue ToJsonValue(Type type, object value)
            {
                return ((DateTime)value).ToString("R");
            }
    
            public object FromJsonValue(Type type, JsonValue value)
            {
                return DateTime.ParseExact((string)value, "R", null);
            }
        }
    
        class Program
        {
            private static JsonContract<User> s_userContract;
            private static JsonContract<Post> s_postContract;
    
            private static JsonContract<Category> s_categoryContract;
    
            static Program()
            {
                s_userContract = new JsonContract<User>();
                s_userContract.SimpleProperty(u => u.UserName).Name("Name");
                s_userContract.SimpleProperty(u => u.Age);
    
                s_postContract = new JsonContract<Post>();
                s_postContract.SimpleProperty(p => p.Title);
                s_postContract.SimpleProperty(p => p.CreateTime).Converter(new DataTimeConverter());
                s_postContract.ArrayProperty(p => p.CurrentUsers).ElementContract(s_userContract);
    
    
    
    
                s_categoryContract = new JsonContract<Category>();
                s_categoryContract.SimpleProperty(p => p.Name);
                //s_categoryContract.ComplexProperty(p => p.Author).Contract(s_userContract);
                //s_categoryContract.ArrayProperty(p => p.Posts).ElementContract(s_postContract);
                s_categoryContract.ArrayProperty(p => p.CurrentPosts).ElementContract(s_postContract);
            }
    
            static void SerializeTest()
            {
                var user = new User { UserName = "Tom", Age = 20 };
                Console.WriteLine(JsonSerializer.SerializeObject(user, s_userContract).ToString());
                Console.WriteLine();
    
                var post = new Post { Title = "Good day today.", CreateTime = DateTime.Now };
                Console.WriteLine(JsonSerializer.SerializeObject(post, s_postContract).ToString());
                Console.WriteLine();
                List<Post> Postss = new List<Post>
                    {
                        new Post { Title = "Post 1", CreateTime = new DateTime(2010, 1, 1),
                            CurrentUsers=new Users(){
                                new User { UserName = "Tom", Age = 20 }
                            } },
                        new Post { Title = "Post 2", CreateTime = new DateTime(2010, 2, 1) },
                        new Post { Title = "Post 3", CreateTime = new DateTime(2010, 3, 1) }
                    };
                var category = new Category
                {
                    Name = "Default",
                    Author = new User { UserName = "Jerry", Age = 15 },
    
                    CurrentPosts = new Posts()
                };
                category.CurrentPosts.AddRange(Postss);
    
                var jsonCategory = JsonSerializer.SerializeObject(category, s_categoryContract);
                Console.WriteLine(jsonCategory.ToString());
                Console.WriteLine();
    
                var category2 = JsonSerializer.DeserializeObject(jsonCategory.ToString(), s_categoryContract);
                var value = new
                {
                    c = JsonSerializer.SerializeObject(category, s_categoryContract),
                    d = user,
                    a = (string)null,
                    p = new object[] { 1, 2, 3, null }
                };
                Console.WriteLine(JsonSerializer.Serialize(value).ToString());
            }
    
            static void DeserializeTest()
            {
                var jsonString = "{ \"name\" : \"hello\", \"age\" : 15, \"a\" : [ 1, 2, 3, 4 ] }";
                var jsonObj = JsonSerializer.Deserialize(jsonString);
    
                var jsonUser = "{ \"Name\" : \"Tom\", \"Age\" : 20 }";
                var user = JsonSerializer.DeserializeObject<User>(jsonUser, s_userContract);
    
                var jsonCategory = "{\"Name\":\"Default\",\"Author\":{\"Name\":\"Jerry\",\"Age\":15},\"Posts\":[{\"Title\":\"Post 1\",\"CreateTime\":\"Fri, 01 Jan 2010 00:00:00 GMT\"},{\"Title\":\"Post 2\",\"CreateTime\":\"Mon, 01 Feb 2010 00:00:00 GMT\"},{\"Title\":\"Post 3\",\"CreateTime\":\"Mon, 01 Mar 2010 00:00:00 GMT\"}]}";
                var category = JsonSerializer.DeserializeObject<Category>(jsonCategory, s_categoryContract);
            }
    
            static void Main(string[] args)
            {
                SerializeTest();
    
                DeserializeTest();
    
                Console.ReadLine();
            }
        }
    }
    
  17. laolaowhn
    122.96.116.*
    链接

    laolaowhn 2011-11-03 15:03:04

    我看错了,我错了

  18. 23232
    124.233.3.*
    链接

    23232 2013-07-18 15:42:42

    可以换一张照片不? V型手势显得你好傻

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我