在托管代码中反序列化 Microsoft Ajax ClientScript JSON






4.56/5 (8投票s)
将 {"d":{"__type": ... JSON 响应无缝反序列化为 CLR 类型。
概述
在上一篇文章中,我提出了一种反序列化来自 ClientScript 端点的 JSON 的方法,例如带有 [ScriptService] 或 [ScriptMethod] 属性的 XML WebServices、支持 Ajax 的 WCF 服务以及使用 WebScriptServiceHostFactory 创建的 WCF 服务。
促成这一需求的使用场景是测试由客户端 JavaScript 调用的端点。使用 HttpWebRequest
在托管代码中调用 WebScript 端点很容易,关键在于指定 content-type 'application/json
'。而问题也随之而来。
问题领域
当 WebScript 端点以 content-type 'application/json
' 响应请求时,它会理所当然地认为它是由 JavaScript 调用,并且(自 3.5sp1 起)会将实际的返回值包装在一个名为 'd
' 的对象中,并为每个对象添加一个 '__type
' 属性。
列表 1:.NET 3.5sp1 JSON 响应
{
"d": {
"__type": "Result:#HttpLibArticleSite",
"Message": "Value pull OK.",
"Session": "rmyykw45zbkxxxzdun0juyfr",
"Value": "foo"
}
}
一个解决方案
在托管代码中反序列化此 JSON 会带来一些挑战。
第一个是 'd
' 包装器。你可以创建包装器类来处理所有你期望反序列化的类型,或者,如上一篇文章所示,创建一个通用的包装器,类似于下面的列表。
列表 2:Ajax 包装器
public class AjaxWrapper<T>
{
public T d;
}
下一个挑战是,如果没有自定义的 JavaScriptTypeResolver 来将 '__type
' 值解析为托管类型,那么 JavaScriptSerializer 和 DataContractJsonSerializer 都无法解析该 JSON。尝试这样做会导致 `ArgumentNull` 异常,因为它找不到 `JavaScriptTypeResolver` 来解析 '__type
' 值。这与我们作为 .Serialize<T>
的泛型参数提供的类型无关。只有当 JSON 未被 `JavaScriptSerializer.ServerTypeFieldName (__type)` 装饰时,类型参数才会被用来实例化 JSON 被填充进去的实例。
我们可以创建一个自定义的 JavaScriptTypeResolver
,但这将涉及解析类型并维护已注册类型的列表,就像运行时所做的那样。这完全超出了我们需求的预期范围。我们只是希望在处理简单的 JSON 时,能够享受默认 JavaScriptSerializer
的行为带来的便利。
因此,一种选择是使用 JSON 库,例如 JSON.net,它不识别 '__type
' 属性,并能愉快地将包装的 JSON 反序列化到 AjaxWrapper
类中。
列表 3:使用 JSON.Net 和 AjaxWrapper
HttpLibArticleSite.Result result = Newtonsoft.Json.JsonConvert.DeserializeObject
<AjaxWrapper<HttpLibArticleSite.Result>>(wrappedJson).d;
如果你已经在项目中使用了 JSON.NET,或者不介意依赖它,那么一切就绪。
更好的解决方案
最近,在开发一个用于测试 JSON 端点的新库时,对 JSON.NET 的依赖变得不那么理想,我开始了又一次尝试,仅使用我自己的代码和内置的框架类型来反序列化包装的 JSON。
如前所述,使用 .NET 序列化器(无论是否带包装器)解析包装的 JSON,如果没有自定义的 JavaScriptTypeResolver
,就会失败。由于我们已经确定自定义 JavaScriptTypeResolver
不符合我们的需求,因此需要另一种方法。
障碍已经确定
- “
d
”包装器 - “
__type
”属性
显而易见的解决方案是消除 JSON 中不符合要求的文本。通常,我发现操纵文本是一项危险的任务,但在此情况下,文本符合 JSON 规范,因此使用 Regex 可以自信地满足要求。
提取内部 JSON 后,我们可以将“__type
”替换为空字符串,然后将 JSON 反序列化到任何形状相似的 CLR 类型。
下面列出了最终的解决方案。ClientScriptJsonUtilities
是一个 static
类,它为 JavaScriptSerializer
提供了一个扩展方法,顺便说一句,该方法在 3.5sp1 中已被取消标记。
注意
某些端点配置会省略“__type
”属性,但仍然使用“d
”进行包装;而其他配置则会返回一个“裸”结果,不包装在“d
”中,但仍包含“__type
”字段;还有一些配置会返回 POJO JSON。
此类将正确处理这些类型的返回,因此 CleanAndDeserialize<T>
可以被视为 `Deserialize<T>` 的向后兼容替代品,在你的代码中随处可用。
匿名类型
现在考虑一个场景,你需要反序列化一个没有对应 CLR 类型的 JSON。你可以简单地定义一个与 JSON 形状相同的类,然后将 JSON 反序列化到该类中。
在需要频繁使用此类型或需要传递响应的情况下,这可能是最佳方法。其他时候,当你要定义的类可以被视为临时类时,使用匿名类型的方法可能更灵活。
在 Jacob Carpenter 的博客的帮助下,我为 CleanAndDeserialize
添加了一个重载,它将接受一个匿名原型。请参阅列表 6。
列表 4:ClientScriptJsonUtilities.cs
// /*!
// * Project: Salient.Web.HttpLib
// * http://salient.codeplex.com
// */
#region
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;
#endregion
namespace Salient.Web.HttpLib
{
public static class ClientScriptJsonUtilities
{
private static readonly Regex RxMsAjaxJsonInner =
new Regex("^{\\s*\"d\"\\s*:(.*)}$", RegexOptions.Compiled);
private static readonly Regex RxMsAjaxJsonInnerType =
new Regex("\\s*\"__type\"\\s*:\\s*\"[^\"]*\"\\s*,\\s*", RegexOptions.Compiled);
/// <summary>
/// Pre-processes <paramref name="json"/>, if necessary,
/// to extract the inner object from a "d:"
/// wrapped MsAjax response and removing "__type"
/// properties to allow deserialization with JavaScriptSerializer
/// into an instance of <typeparamref name="T"/>.
///
/// Note: this method is not limited to MsAjax responses,
/// it will capably deserialize any valid JSON.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="serializer"></param>
/// <param name="json"></param>
/// <returns></returns>
public static T CleanAndDeserialize<T>
(this JavaScriptSerializer serializer, string json)
{
string innerJson = CleanWebScriptJson(json);
return serializer.Deserialize<T>(innerJson);
}
/// <summary>
/// Pre-processes <paramref name="json"/>, if necessary,
/// to extract the inner object from a "d:"
/// wrapped MsAjax response and removing "__type" properties
/// to allow deserialization with JavaScriptSerializer
/// into an instance of anonymous type <typeparamref name="T"/>.
///
/// Note: this method is not limited to MsAjax responses,
/// it will capably deserialize any valid JSON.
/// </summary>
/// <typeparam name="T">The anonymous type defined by
/// <paramref name="anonymousPrototype"/> </typeparam>
/// <param name="serializer"></param>
/// <param name="json"></param>
/// <param name="anonymousPrototype">
/// An instance of the anonymous type into which
/// you would like to stuff this JSON. It simply needs to be
/// shaped like the JSON object.
/// <example>
/// string json = "{ \"name\": \"Joe\" }";
/// var jsob = new JavaScriptSerializer().CleanAndDeserialize
/// (json, new { name=default(string) });
/// Debug.Assert(jsob.name=="Joe");
/// </example>
/// </param>
/// <returns></returns>
public static T CleanAndDeserialize<T>
(this JavaScriptSerializer serializer, string json, T anonymousPrototype)
{
json = CleanWebScriptJson(json);
Dictionary<string, object> dict = (Dictionary<string,
object>)serializer.DeserializeObject(json);
return dict.ToAnonymousType(anonymousPrototype);
}
/// <summary>
/// Extracts the inner JSON of an MS Ajax 'd' result and
/// removes embedded '__type' properties.
/// </summary>
/// <param name="json"></param>
/// <returns>The inner JSON</returns>
private static string CleanWebScriptJson(string json)
{
if (string.IsNullOrEmpty(json))
{
throw new ArgumentNullException("json");
}
Match match = RxMsAjaxJsonInner.Match(json);
string innerJson = match.Success ? match.Groups[1].Value : json;
return RxMsAjaxJsonInnerType.Replace(innerJson, string.Empty);
}
#region Dictionary to Anonymous Type
/* An entry on Jacob Carpenter saved me from having to work this out for myself.
* Thanks Jacob.
* http://jacobcarpenter.wordpress.com/2008/03/13/dictionary-to-anonymous-type/
*/
/// <summary>
///
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="dict"></param>
/// <param name="key"></param>
/// <returns></returns>
private static TValue GetValueOrDefault<TKey, TValue>
(this IDictionary<TKey, TValue> dict, TKey key)
{
TValue result;
dict.TryGetValue(key, out result);
return result;
}
private static T ToAnonymousType<T, TValue>
(this IDictionary<string, TValue> dict, T anonymousPrototype)
{
// get the sole constructor
var ctor = anonymousPrototype.GetType().GetConstructors().Single();
// conveniently named constructor parameters make this all possible...
// TODO: sky: i think the conditional assignment could be improved
// ReSharper disable CompareNonConstrainedGenericWithNull
// In our typical use of this method, we are deserializing valid json,
// which should not contain
// nulls for value types. So this is not a problem.
var args = from p in ctor.GetParameters()
let val = dict.GetValueOrDefault(p.Name)
select val != null &&
p.ParameterType.IsAssignableFrom(val.GetType()) ?
(object)val : null;
// ReSharper restore CompareNonConstrainedGenericWithNull
return (T)ctor.Invoke(args.ToArray());
}
#endregion
}
}
列表 5:CleanAndDeserialize<T>() 用法
Result result = new JavaScriptSerializer().CleanAndDeserialize<Result>(responseText);
列表 6:使用匿名类型的 CleanAndDeserialize() 用法
[Test]
public void CleanAndDeserializeToAnonymousType()
{
// To demonstrate deserializing to an anonymous type consider this:
// This is an example of a TestClass response
// {"d":{"__type":"TestClass:#Salient.Web.HttpLib.TestSite",
// "Date":"\/Date(1271275580882)\/","Header":"","IntVal":99,"Name":"sky"}}
const string responseText =
"{\"d\":{\"__type\":\"TestClass:#Salient.Web.HttpLib.TestSite\",\"Date\":\"\\
/Date(1271275580882)\\/\",\"Header\":\"\",\"IntVal\":99,\"Name\":\"sky\"}}";
// imagine for a moment that we do not have a reference to
// Salient.Web.HttpLib.TestSite from which
// to deserialize this JSON into an instance of TestClass.
// this is what an anonymous prototype of TestClass looks like
var testClassPrototype = new
{
Name = default(string),
Header = default(string),
Date = default(DateTime),
IntVal = default(int)
};
// just pass this prototype to deserialize to get a strongly typed instance
var jsob = new JavaScriptSerializer().CleanAndDeserialize
(responseText, testClassPrototype);
Assert.AreEqual("sky", jsob.Name);
Assert.AreEqual(99, jsob.IntVal);
// now also imagine that we are interested in only part of a JSON response,
var prototypeOfInterestingData = new
{
Name = default(string)
};
var partialJsob = new JavaScriptSerializer().CleanAndDeserialize
(responseText, prototypeOfInterestingData);
Assert.AreEqual("sky", partialJsob.Name);
// one thing to keep in mind is that anonymous types are read-only.
}
历史
- 2010-04-15 - 添加了匿名类型支持
- 2010-04-18 - 移除了许可限制
你可以在 http://salient.codeplex.com 找到最新的源代码和测试。