55

I'm trying to write a C# method that will serialize a model and return a JSON result. Here's my code:

    public ActionResult Read([DataSourceRequest] DataSourceRequest request)
    {
        var items = db.Words.Take(1).ToList();
        JsonSerializerSettings jsSettings = new JsonSerializerSettings();
        jsSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        var converted = JsonConvert.SerializeObject(items, null, jsSettings);
        return Json(converted, JsonRequestBehavior.AllowGet);
    }

I got the following JSON result when I go to Words/Read in Chrome:

"[{\"WordId\":1,\"Rank\":1,\"PartOfSpeech\":\"article\",\"Image\":\"Upload/29/1/Capture1.PNG\",\"FrequencyNumber\":\"22038615\",\"Article\":null,\"ClarificationText\":null,\"WordName\":\"the | article\",\"MasterId\":0,\"SoundFileUrl\":\"/UploadSound/7fd752a6-97ef-4a99-b324-a160295b8ac4/1/sixty_vocab_click_button.mp3\",\"LangId\":1,\"CatId\":null,\"IsActive\":false}

I think the \" escaped quotes are a problem that occurs when you double serialize an object. From other questions: WCF JSON output is getting unwanted quotes & backslashes added

It definitely looks like I'm double serializing my object, since I first serialize using JSON.NET and then pass my result into the Json() function. I need to manually serialize to avoid referenceloops, but I think my View needs an ActionResult.

How can I return an ActionResult here? Do I need to, or can I just return a string?

Community
  • 1
  • 1
hubatish
  • 5,070
  • 6
  • 35
  • 47

2 Answers2

89

I found a similar stackoverflow question: Json.Net And ActionResult

The answer there suggested using

return Content( converted, "application/json" );

That seems to work on my very simple page.

Community
  • 1
  • 1
hubatish
  • 5,070
  • 6
  • 35
  • 47
  • 4
    IMO best answer. Simple and does exactly what was asked. – pim Mar 13 '15 at 19:35
  • 2
    if you will check the headers, you will see you probably have `text/html` as content type. – Rafael Herscovici Oct 31 '15 at 17:25
  • By far the simplest solution. This does exactly what it needs to. – user1751825 Jan 03 '16 at 23:08
  • Based on hubatish's answer: public abstract class ControllerBase : Controller { protected const string JsonEncoding = "application/json; charset=utf-8"; protected ContentResult JsonNetResult(object result) { string json = JsonConvert.SerializeObject(result); return Content(json, JsonEncoding); } } – Roman M Feb 21 '18 at 21:40
  • @Dementic Maybe something has changed in 4 years but using the syntax in this answer my response headers show `content-type: application/json`. – Philip Stratford Apr 24 '20 at 14:15
69

Instead of serializing using JSON.NET and then calling Json(), why not instead override the Json() method in your controller (or perhaps a base controller to enhance its re-usability)?

This is pulled from this blog post.

In your controller (or base controller):

protected override JsonResult Json(
        object data,
        string contentType,
        System.Text.Encoding contentEncoding,
        JsonRequestBehavior behavior)
{
    return new JsonNetResult
    {
        Data = data,
        ContentType = contentType,
        ContentEncoding = contentEncoding,
        JsonRequestBehavior = behavior
    };
}

And the definition for JsonNetResult:

public class JsonNetResult : JsonResult
{
    public JsonNetResult()
    {
        Settings = new JsonSerializerSettings
        {
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        };
    }

    public JsonSerializerSettings Settings { get; private set; }

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


        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType =
            string.IsNullOrEmpty(this.ContentType) ? "application/json" : this.ContentType;

        if (this.ContentEncoding != null)
            response.ContentEncoding = this.ContentEncoding;
        if (this.Data == null)
            return;

        var scriptSerializer = JsonSerializer.Create(this.Settings);

        using (var sw = new StringWriter())
        {
            scriptSerializer.Serialize(sw, this.Data);
            response.Write(sw.ToString());
        }
    }
}

By doing this, when you call Json() in your controller, you will automatically get the JSON.NET serializing you want.

jpaugh
  • 6,634
  • 4
  • 38
  • 90
Sven Grosen
  • 5,616
  • 3
  • 30
  • 52
  • Cool. This is pretty convenient. Also, I was thinking this would allow me to still pass JSONRequestBehavior.AllowGet, but it doesn't quite seem to be working. – hubatish Apr 28 '14 at 19:18
  • To get that to work, you'd need to add overrides to `Json()` that pass in default values for the other parameters. This is what the default implementation does. – Sven Grosen Apr 28 '14 at 19:23
  • Well, this line Json(object data, string contentType, System.Text.Encoding contentEncoding, JsonRequestBehavior behavior) seems to mean I can do Json(converted,JsonRequestBehavior.AllowGet); but I get "To allow GET requests, set JsonRequestBehavior to AllowGet." when I go to the page. – hubatish Apr 28 '14 at 19:26
  • 2
    Just pass in null for `contentType` and `contentEncoding` and you'll get the same results as the default implementation. Otherwise add an overload to `Json()` that only takes `data` and `behavior`, and provide null values for `contentType` and `contentEncoding`, and then call the implementation with all parameters I have in my answer. – Sven Grosen Apr 28 '14 at 19:29
  • The answer by @hubatish himself is better. – Fayyaz Naqvi Dec 02 '15 at 11:36
  • 2
    The JsonNetResult's enforcement of AllowGet seems redundant with the attributes applied to the mehtod - it surprised me that I had to redundantly enable AllowGet when I already have AcceptVerbsAttribute. Need to find out how to read AcceptVerbsAttribute from the ControllerContext. – Pxtl Nov 04 '16 at 23:08
  • 1
    Great response. My question is in regards to the very last part: is it better to serialize everything to a StringWriter and then write to the HTTP response, or would writing directly to the output stream be more efficient? `using (var sw = new StreamWriter(response.OutputStream, response.ContentEncoding)) { scriptSerializer.Serialize(sw, Data); sw.Flush(); }` – Joshua Jan 05 '17 at 22:56