newtonsoft.json 序列化踩坑之 ienumerable

intro

newtonsoft.json 是 .net 下最受欢迎 json 操作库,使用起来也是非常方便,有时候也可能会不小心就踩坑了,这次就踩了一个,坑是这样的,如果要序列化的对象实现了 ienumerable 接口,newtonsoft.json 就会认为这个对象是一个数组。。然后遍历这个对象,输出其中的值,如果是一个自定义的类型而且还有其他属性,其他属性就会被忽略,序列化之后就会发生数据丢失。

问题代码

在我的公用类库 weihanli.common 有一个分页列表的model:

在 1.0.21及之前版本是这样定义的 源码

using system;
using system.collections;
using system.collections.generic;

namespace weihanli.common.models
{
    /// <summary>
    /// ipagedlistmodel
    /// </summary>
    /// <typeparam name="t">type</typeparam>
    public interface ipagedlistmodel<out t> : ireadonlylist<t>
    {
        /// <summary>
        /// data
        /// </summary>
        ireadonlylist<t> data { get; }

        /// <summary>
        /// pagenumber
        /// </summary>
        int pagenumber { get; }

        /// <summary>
        /// pagesize
        /// </summary>
        int pagesize { get; }

        /// <summary>
        /// totaldatacount
        /// </summary>
        int totalcount { get; set; }
    }

    /// <inheritdoc />
    /// <summary>
    /// 分页model
    /// </summary>
    /// <typeparam name="t">type</typeparam>
    [serializable]
    public class pagedlistmodel<t> : ipagedlistmodel<t>
    {
        public ireadonlylist<t> data { get; set; }

        private int _pagenumber = 1;

        public int pagenumber
        {
            get => _pagenumber;
            set
            {
                if (value > 0)
                {
                    _pagenumber = value;
                }
            }
        }

        private int _pagesize = 10;

        public int pagesize
        {
            get => _pagesize;
            set
            {
                if (value > 0)
                {
                    _pagesize = value;
                }
            }
        }

        private int _totalcount;

        public int totalcount
        {
            get => _totalcount;
            set
            {
                if (value > 0)
                {
                    _totalcount = value;
                }
            }
        }

        public int pagecount => convert.toint32(math.ceiling(_totalcount * 1.0 / _pagesize));

        public ienumerator<t> getenumerator()
        {
            return data.getenumerator();
        }

        ienumerator ienumerable.getenumerator()
        {
            return data.getenumerator();
        }

        public t this[int index] => data[index];

        public int count => data.count;
    }
}

上面的这种定义相当于实现了 ienumerable 接口,之所以实现这个接口,是因为可以直接遍历这个对象,不需要遍历这个对象的data 属性上遍历,但是这样序列化的时候就会有问题, pagenumber/pagesize/totalpage 之类的信息序列化时就会丢失

solution

不要实现 ienumerable 接口就可以了,修改后的代码如下所示:

using system;
using system.collections.generic;

namespace weihanli.common.models
{
    /// <summary>
    /// ipagedlistmodel
    /// </summary>
    /// <typeparam name="t">type</typeparam>
    public interface ipagedlistmodel<out t>
    {
        /// <summary>
        /// data
        /// </summary>
        ireadonlylist<t> data { get; }

        /// <summary>
        /// pagenumber
        /// </summary>
        int pagenumber { get; }

        /// <summary>
        /// pagesize
        /// </summary>
        int pagesize { get; }

        /// <summary>
        /// totaldatacount
        /// </summary>
        int totalcount { get; set; }
    }

    /// <inheritdoc />
    /// <summary>
    /// 分页model
    /// </summary>
    /// <typeparam name="t">type</typeparam>
    [serializable]
    public class pagedlistmodel<t> : ipagedlistmodel<t>
    {
        public ireadonlylist<t> data { get; set; }

        private int _pagenumber = 1;

        public int pagenumber
        {
            get => _pagenumber;
            set
            {
                if (value > 0)
                {
                    _pagenumber = value;
                }
            }
        }

        private int _pagesize = 10;

        public int pagesize
        {
            get => _pagesize;
            set
            {
                if (value > 0)
                {
                    _pagesize = value;
                }
            }
        }

        private int _totalcount;

        public int totalcount
        {
            get => _totalcount;
            set
            {
                if (value > 0)
                {
                    _totalcount = value;
                }
            }
        }

        public int pagecount => convert.toint32(math.ceiling(_totalcount * 1.0 / _pagesize));

        public t this[int index] => data[index];

        public int count => data.count;
    }
}

test

写个示例测试一下,原来的代码类型改为 pagedlistmodel1 ,测试代码如下:

pagedlistmodel1:

using system;
using system.collections;
using system.collections.generic;
using system.text;

namespace dotnetcoresample.test
{
    public class pagedlistmodel1<t> : ienumerable<t>
    {
        public ireadonlylist<t> data { get; set; }

        private int _pagenumber = 1;

        public int pagenumber
        {
            get => _pagenumber;
            set
            {
                if (value > 0)
                {
                    _pagenumber = value;
                }
            }
        }

        private int _pagesize = 10;

        public int pagesize
        {
            get => _pagesize;
            set
            {
                if (value > 0)
                {
                    _pagesize = value;
                }
            }
        }

        private int _totalcount;

        public int totalcount
        {
            get => _totalcount;
            set
            {
                if (value > 0)
                {
                    _totalcount = value;
                }
            }
        }

        public int pagecount => convert.toint32(math.ceiling(_totalcount * 1.0 / _pagesize));

        public t this[int index] => data[index];

        public int count => data.count;

        public ienumerator<t> getenumerator()
        {
            return data.getenumerator();
        }

        ienumerator ienumerable.getenumerator()
        {
            return data.getenumerator();
        }
    }
}

测试代码:

var pagedlistmodel = new pagedlistmodel<int>()
            {
                pagenumber = 2, pagesize = 2, totalcount = 6, data = new int[] {1, 2},
            };
var pagedlistmodel1 = new pagedlistmodel1<int>()
            {
                pagenumber = 2,
                pagesize = 2,
                totalcount = 6,
                data = new int[] { 1, 2 },
            };
console.writeline($"pagedlistmodel:{jsonconvert.serializeobject(pagedlistmodel)}, pagedlistmodel1:{jsonconvert.serializeobject(pagedlistmodel1)}");

output:

pagedlistmodel:{"data":[1,2],"pagenumber":2,"pagesize":2,"totalcount":6,"pagecount":3,"count":2}, pagedlistmodel1:[1,2]

可以看到实现了 ienumerable 接口的那个类序列化之后一些属性丢失了

research

查看 newtonsoft.json 源码 https://github.com/jamesnk/newtonsoft.json
,找到为什么实现了 ienumerable 接口就会有问题,最后找到了这里 https://github.com/jamesnk/newtonsoft.json/blob/master/src/newtonsoft.json/serialization/defaultcontractresolver.cs#l1218

可以看到只要实现了 ienumerable 接口,就会被当作是一个json 数组,foreach 遍历其中的元素,其他属性就会被忽略掉了,这就是为什么上面我们实现了 ienumerable 接口的对象序列化之后发生属性丢失的原因。

reference

  • https://github.com/jamesnk/newtonsoft.json
  • https://github.com/jamesnk/newtonsoft.json/blob/master/src/newtonsoft.json/serialization/defaultcontractresolver.cs#l1218
  • https://github.com/weihanli/weihanli.common/blob/dev/samples/dotnetcoresample/program.cs