1

Long ago, I set a coding standard for my app that all actions returning JSON would have their results put into a top-level wrapper object:

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

That has worked well, and provided me with good code standardization happiness.

Today, however, I realized that my server-side code assumes that it always gets to do the full serialization of what's getting returned. Now I would like to serialize one of these guys where the "data" payload is already a well-formed JSON string of its own.

This is the general pattern that had been working:

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;

Where it breaks is that the "data" element will be received as a string when it gets to the browser, and not as the proper JSON object (or array in the example above) it should be.

Is there some way I can decorate a property with an attribute that says "serialize as raw", or am I in the realm of writing a custom JSON serializer to make this work?

Eric
  • 121
  • 1
  • 8
  • 1
    You have to use `json.parse`. Read full details here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse – Pratik Gaikwad Dec 08 '16 at 23:05
  • 1
    Well, technically you're breaking your own contract and standard. Previously you're expecting an *object*, now you're passing it a string. It looks to me like it's a bad way to go about things, but you *could* write `data = JsonSerializer.Deserialize(jsonData)` if there's no way to avoid this string. Do note, though, that you'll be serializing it only to have it deserialized later. – Rob Dec 08 '16 at 23:05
  • json.parse is on the browser end. I'm trying to manipulate the serialization on the server side. Rob, you're exactly onto the problem.. I don't want to DEserialize it, just to then immediately REserialize it (and really just to put a thin wrapper around what was already perfectly good JSON). I think I've pulled together a solution, though, and will post that tomorrow. Thank you both! – Eric Dec 09 '16 at 01:06

4 Answers4

1

You're serializing it twice (jsonData + output). You can't do that and expect to only deserialize it once (output).

You could set the "data" object in your dynamic to be the real c# object, that would work. Or you can re-name your property to "jsonData":

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

...so it reflects what you're really doing :).

Jerod Venema
  • 44,124
  • 5
  • 66
  • 109
  • Thanks, jvenema, I appreciate the response. My trouble is that this isn't really a C# object. It's actually coming straight from SQL as a JSON string. I don't even have a type anywhere in my middle-tier that equates to it. I'm just trying to have the web server be a pass-through. That said - I think I've pulled together a solution with a custom serializer, based on a couple of other threads. If it works, I'll post what I came up with tomorrow. – Eric Dec 09 '16 at 00:57
1

I would think that you just need to serialize the string being returned from the SQL table into an object, using a JSON serializer, like NewtonSoft.

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;
Derek Van Cuyk
  • 953
  • 7
  • 23
1

Here's what I ended up with....

// 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:
//   http://stackoverflow.com/questions/17244774/proper-json-serialization-in-mvc-4
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);

With this, I'm not doing any Json parsing on the server. My string comes out of the database and goes straight to the client without MVC touching it.

EDIT: Updated version now also supports serializing objects that have individual properties somewhere in their hierarchy that are of type JsonStringWrapper. This is useful in my scenario to support a "hybrid" model. If object A has a property B that is one of my pre-baked JSON strings, the code above will properly handle that.

Eric
  • 121
  • 1
  • 8
0

You could accomplish this by forming the JSON package yourself using the JsonWriter class from Newtonsoft. It would look something like this:

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;
}
Michael Weinand
  • 401
  • 2
  • 4