在 MVC 操作输出中自定义 JSON 序列化

Customizing JSON serialization in MVC action output

很久以前,我为我的应用程序设置了一个编码标准,所有返回 JSON 的操作都会将其结果放入顶级包装器对象中:

var result = {
    success: false,
    message: 'Something went wrong',
    data: {} // or []
}

效果很好,给我带来了代码标准化的快乐。

然而,今天,我意识到我的服务器端代码假定它总是对返回的内容进行完整序列化。现在我想序列化其中一个人,其中 "data" 有效载荷已经是一个格式正确的 JSON 字符串。

这是一直有效的一般模式:

bool success = false;
string message = "Something went wrong";
object jsonData = "[{\"id\":\"0\",\"value\":\"1234\"}]";  // Broken

dynamic finalData = new { success = success, message = message, data = jsonData };

JsonResult output = new JsonResult
{
    Data = finalData,
    JsonRequestBehavior = JsonRequestBehavior.AllowGet,
    MaxJsonLength = int.MaxValue
};
return output;

问题在于 "data" 元素在到达浏览器时将作为字符串接收,而不是作为正确的 JSON 对象(或上例中的数组)接收应该是。

有没有什么方法可以用 "serialize as raw" 的属性装饰 属性,或者我是否正在编写自定义 JSON 序列化程序来完成这项工作?

您将其序列化两次(jsonData + 输出)。你不能那样做并期望只反序列化一次(输出)。

您可以将动态中的 "data" 对象设置为真正的 c# 对象,这样就可以了。或者您可以将 属性 重命名为 "jsonData":

dynamic finalData = new { success = success, message = message, jsonData = jsonData };

...所以它反映了你真正在做什么:)。

您可以使用 Newtonsoft 的 JsonWriter class 自己构建 JSON 包来完成此操作。它看起来像这样:

using(var textWriter = new StringWriter())
using(var jsonWriter = new JsonTextWriter(textWriter))
{
   jsonWriter.WriteStartObject();

   jsonWriter.WritePropertyName("success");
   jsonWriter.WriteValue(success);

   jsonWriter.WritePropertyName("message");
   jsonWriter.WriteValue(message);

   jsonWriter.WritePropertyName("data");
   jsonWriter.WriteRaw(jsonData);

   jsonWriter.WriteEndObject();

   var result = new ContentResult();
   result.Content = textWriter.ToString();
   result.ContentType = "application/json";
   return result;
}

我认为您只需要使用 JSON 序列化程序(例如 NewtonSoft)将从 SQL table 返回的字符串序列化为一个对象。

bool success = false;
string message = "Something went wrong";
string rawData = "[{\"id\":\"0\",\"value\":\"1234\"}]";  // Broken
object jsonData = JsonConvert.DeserializeObject<dynamic>(rawData);

dynamic finalData = new { success = success, message = message, data = jsonData };

JsonResult output = new JsonResult
{
    Data = finalData,
    JsonRequestBehavior = JsonRequestBehavior.AllowGet,
    MaxJsonLength = int.MaxValue
};
return output;

这是我最终得到的......

// Wrap "String" in a container class
public class JsonStringWrapper
{
    // Hey Microsoft - This is where it would be nice if "String" wasn't marked "sealed"
    public string theString { get; set; }
    public JsonStringWrapper() { }
    public JsonStringWrapper(string stringToWrap) { theString = stringToWrap; }
}

// Custom JsonConverter that will just dump the raw string into
// the serialization process.  Loosely based on:
//   http://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm
public class JsonStringWrapperConverter : JsonConverter
{
    private readonly Type _type = typeof(JsonStringWrapper);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken t = JToken.FromObject(value);

        if (t.Type != JTokenType.Object)
        {
            t.WriteTo(writer);
        }
        else
        {
            string rawValue = ((JsonStringWrapper)value).theString;
            writer.WriteRawValue((rawValue == null) ? "null" : rawValue);
        }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanConvert(Type objectType)
    {
        return _type == objectType;
    }
}

// Custom JsonResult that will use the converter above, largely based on:
//   
public class PreSerializedJsonResult : JsonResult
{
    private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
    {
        Converters = new List<JsonConverter> { new JsonStringWrapperConverter() }
    };

    public override void ExecuteResult(ControllerContext context)
    {
        if (this.JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
            string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException("GET request not allowed");
        }

        var response = context.HttpContext.Response;

        response.ContentType = !string.IsNullOrEmpty(this.ContentType) ? this.ContentType : "application/json";

        if (this.ContentEncoding != null)
        {
            response.ContentEncoding = this.ContentEncoding;
        }

        if (this.Data == null)
        {
            return;
        }

        response.Write(JsonConvert.SerializeObject(this.Data, Settings));
    }
}

// My base controller method that overrides Json()...
protected JsonResult Json(string message, object data)
{
    PreSerializedJsonResult output = new PreSerializedJsonResult();

    object finalData = (data is string && (new char[] { '[', '{' }.Contains(((string)data).First())))
        ? new JsonStringWrapper(data as string)
        : data;

    output.Data = new
    {
        success = string.IsNullOrEmpty(message),
        message = message,
        data = finalData
    };
    output.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
    output.MaxJsonLength = int.MaxValue;
    return output;
}

// Aaaand finally, here's how it might get called from an Action method:
...
return Json("This was a failure", null);
...
return Json(null, yourJsonStringVariableHere);

这样,我就不会在服务器上进行任何 Json 解析。我的字符串从数据库中出来并直接进入客户端,而 MVC 没有接触它。

编辑:更新版本现在还支持序列化对象,这些对象在层次结构中某处具有 JsonStringWrapper 类型的单独属性。这在我的场景中很有用,可以支持 "hybrid" 模型。如果对象 A 有一个 属性 B,它是我预先烘焙的 JSON 字符串之一,上面的代码将正确处理它。