Using JSON.NET with ASP.NET Web API


Json.Net is a popular framework for working with JSON. In particular, it has a bunch of features that are not supported by the DataContractJsonSerializer such as being much more flexible in what kind of types it can serialize and exactly how they should be serialized. The ASP.NET Web API supports an open-ended set of formatters that can read and write data to and from any media type you want to support. For example, if you want to support the vCard format which has the media type text/vcard (previously it was text/directory) media type then you can write a formatter for vCard and register it for the media type (or types) in question.

Note: JSON.NET is now an integral part of ASP.NET Web API so you can just us it out of the box. 

This sample shows how to hook in Json.Net as the default formatter replacing the built in DataContractJsonSerializer formatter (in the beta bits DataContractJsonSerializer is the default formatter). There are already a bunch of Json.Net formatters provided by the community that may well be more full-featured but this should allow you to get started.

Building the Formatter

The first thing we do is building the formatter. The key part of the formatter is to provide support for reading and writing content of a given media type. The sample formatter derives from the base MediaTypeFormatter class; we are working on a buffered media type formatter that will help working with lots of small reads and writes but for this sample the goal is to keep things simple. The formatter looks like this:  

   1: public class JsonNetFormatter : MediaTypeFormatter
   2: {
   3:     private JsonSerializerSettings _jsonSerializerSettings;
   4:  
   5:     public JsonNetFormatter(JsonSerializerSettings jsonSerializerSettings)
   6:     {
   7:         _jsonSerializerSettings = jsonSerializerSettings ?? new JsonSerializerSettings();
   8:  
   9:         // Fill out the mediatype and encoding we support
  10:         SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
  11:         Encoding = new UTF8Encoding(false, true);
  12:     }
  13:  
  14:     protected override bool CanReadType(Type type)
  15:     {
  16:         if (type == typeof(IKeyValueModel))
  17:         {
  18:             return false;
  19:         }
  20:  
  21:         return true;
  22:     }
  23:  
  24:     protected override bool CanWriteType(Type type)
  25:     {
  26:         return true;
  27:     }
  28:  
  29:     protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)
  30:     {
  31:         // Create a serializer
  32:         JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
  33:  
  34:         // Create task reading the content
  35:         return Task.Factory.StartNew(() =>
  36:         {
  37:             using (StreamReader streamReader = new StreamReader(stream, Encoding))
  38:             {
  39:                 using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader))
  40:                 {
  41:                     return serializer.Deserialize(jsonTextReader, type);
  42:                 }
  43:             }
  44:         });
  45:     }
  46:  
  47:     protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)
  48:     {
  49:         // Create a serializer
  50:         JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
  51:  
  52:         // Create task writing the serialized content
  53:         return Task.Factory.StartNew(() =>
  54:         {
  55:             using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(stream, Encoding)) { CloseOutput = false })
  56:             {
  57:                 serializer.Serialize(jsonTextWriter, value);
  58:                 jsonTextWriter.Flush();
  59:             }
  60:         });
  61:     }
  62: }

Building a Sample ApiController

Next we need a controller to try things out. For illustrative purposes we create a type that would not serialize well with DataContractJsonSerializer but other than that this is a completely vanilla controller that knows nothing about serialization:

   1: public class HomeController : ApiController
   2: {
   3:     public HomeInfo Get()
   4:     {
   5:         return new HomeInfo();
   6:     }
   7: }
   8:  
   9: public class HomeInfo
  10: {
  11:     private readonly DateTime _created = DateTime.UtcNow;
  12:     private readonly Dictionary<int, string> _colorMap = new Dictionary<int, string>
  13:     {
  14:         { 1, "blue"},
  15:         { 2, "red" },
  16:         { 3, "green" },
  17:         { 4, "black" },
  18:         { 5, "white" },
  19:     };
  20:  
  21:     public DateTime Created { get { return _created; } }
  22:  
  23:     public IDictionary<int, string> ColorMap { get { return _colorMap; } }
  24: }

Hosting the Controller

Now that we have the controller we can host it in either ASP or as selfhost. In this case we selfhost the controller in a simple console application but it would work exactly the same if hosted in ASP.

The first part is to configure the selfhost server and injecting the JsonNetFormatter as the first formatter in the configuration so that it becomes the default formatter. We also configure Json.Net to serialize DateTime types using ISO 8601 format instead of the more esoteric "/Date(1240718400000)/” format. The part of the console app that configures and starts the server looks like this:

   1: // Set up server configuration
   2: HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("http://localhost:8080");
   3: config.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Home" });
   4:  
   5: // Create Json.Net formatter serializing DateTime using the ISO 8601 format
   6: JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
   7: serializerSettings.Converters.Add(new IsoDateTimeConverter());
   8: config.Formatters[0] = new JsonNetFormatter(serializerSettings);
   9:  
  10: // Create server
  11: server = new HttpSelfHostServer(config);
  12:  
  13: // Start listening
  14: server.OpenAsync().Wait();

Note: In order to successfully start the selfhost server you have to run as admin (or configure http.sys with the appropriate URI prefix using netsh).

Trying it Out

Once the controller is running, we can access it using any HTTP client. In this case we use HttpClient to access it and print out the result to the console. If we put both the server configuration and the client in the same Main then we get something like this:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         HttpSelfHostServer server = null;
   6:         try
   7:         {
   8:             // Set up server configuration
   9:             HttpSelfHostConfiguration config = new HttpSelfHostConfiguration("http://localhost:8080");
  10:             config.Routes.MapHttpRoute("Default", "{controller}", new { controller = "Home" });
  11:  
  12:             // Create Json.Net formatter serializing DateTime using the ISO 8601 format
  13:             JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
  14:             serializerSettings.Converters.Add(new IsoDateTimeConverter());
  15:             config.Formatters[0] = new JsonNetFormatter(serializerSettings);
  16:  
  17:             // Create server
  18:             server = new HttpSelfHostServer(config);
  19:  
  20:             // Start listening
  21:             server.OpenAsync().Wait();
  22:  
  23:             // Create HttpClient, do an HTTP GET on the controller, and show the output
  24:             HttpClient client = new HttpClient();
  25:             client.GetAsync("http://localhost:8080").ContinueWith(
  26:                 (requestTask) =>
  27:                 {
  28:                     // Get HTTP response from completed task.
  29:                     HttpResponseMessage response = requestTask.Result;
  30:  
  31:                     // Check that response was successful or throw exception
  32:                     response.EnsureSuccessStatusCode();
  33:  
  34:                     // Read response asynchronously as string and write out
  35:                     response.Content.ReadAsStringAsync().ContinueWith(
  36:                         (readTask) =>
  37:                         {
  38:                             Console.WriteLine(readTask.Result);
  39:                         });
  40:                 });
  41:  
  42:             Console.WriteLine("Hit ENTER to exit...");
  43:             Console.ReadLine();
  44:  
  45:         }
  46:         finally
  47:         {
  48:             if (server != null)
  49:             {
  50:                 // Stop listening
  51:                 server.CloseAsync().Wait();
  52:             }
  53:         }
  54:     }
  55: }

The resulting output written to the console is

{"Created":"2012-02-18T00:54:06.8447642Z","ColorMap":{"1":"blue","2":"red","3":"green","4":"black","5":"white"}}

Note the ISO date and the nice serialization of the dictionary!

Henrik

del.icio.us Tags: ,,,,
Comments (35)

  1. Gabe Perez says:

    Hey Henrik, I was wondering why there isn't any interfaces for the HttpClient? I'd like to inject it as a dependency in my classes and an interface would make possible for me to mock it using something like Moq.

    Thanks!

  2. We don't have interfaces but you can actually just new up the HttpRequestMessage and HttpResponseMessage etc. types directly and use them in your tests without a lot of mocking.

  3. Terrence says:

    Henrik, I am having trouble on the client side.  Can you create some examples of a client sending  CRUD calls using HttpClient?

    Thanks.

  4. Terrence, Sure, what in particular are you looking for?

  5. Piers Lawson says:

    If you do get to support JSON.Net out of the box and the automatically generated help pages with embedded XML schemas, you may want to consider embedding JSON schemas as well.

  6. Terrence says:

    Well I am working on a data app for the windows store.  I need an example of submitting a poco to add, update and delete using the httpclient.   I know it may seem obvious, but I am a simpleton and nothing  is obvious to me with these new technologies.  Thanks or any help you can provide.

  7. Gabe Perez says:

    Hey Henrik, thanks for answering. I got some Glenn Block on twitter on how to deal with it. I've blogged about it so maybe it will help the next person that comes along.

    gbogea.com/…/web-api-testing-with-httpclient

  8. Darren says:

    Are there any examples out there posting files to the new Web Api with Form Data?  For example in MVC 3 you can have a model with properties and one property with the HttpPostedFileWrapper.  I haven't been able to get this to work with the ApiController

  9. I would like to use my traditional services using ASP.NET MVC4 Web API (i.e. i would like to replace WCF Web API), can you please provide blog post on this.

    Thanks a million in advance.

  10. This code doesn't work for me. It sometimes throws a HttpException and terminates IIS Express: OnWriteToStreamAsync => "The remote host closed the connection. The error code is 0x800704CD." A first chance exception of type 'System.Web.HttpException' occurred in System.Web.dll. But it seems to me I've found the solution in your JsonMediaTypeFormatter class: return TaskHelpers.RunSynchronously(…);

  11. Unfortunately, I was wrong. This is some other problems. Here is the last three stack trace entries:

      at System.Web.Hosting.IIS7WorkerRequest.RaiseCommunicationError(Int32 result, Boolean throwOnDisconnect)

      at System.Web.Hosting.IIS7WorkerRequest.ExplicitFlush()

      at System.Web.HttpResponse.Flush(Boolean finalFlush)

  12. Piers Lawson says:

    Could you provide some hints as to how I could develop a MediaTypeFormatter that passed control to the standard ASP.Net MVC rendering engine so that I could easily support text/html along side application/xml, application/json?

  13. Unfortunately there was a bug in JsonNetFormatter.OnWriteToStreamAsync in the formatter when used on the client side causing the stream to be closed too early. The blog has been updated to contain a working version of OnWriteToStreamAsync. Sorry about that!

  14. I have tried it but it is some other problem. JsonTextWriter's Flush/Serialize method also throws a HttpException. Moreover, it sometimes stops IIS Express: "A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread." (IIS7WorkerRequest – RaiseCommunicationError). It is worth to mention that this code (without async support) works fine with WCF Web API preview versions.

  15. Daniel Wertheim says:

    Hi,

    skimming through your article I can'f find where the Configuration is to be acquired in an ASP hosted solution. Guessing GlobalConfiguration.Configuratioin and then add the custom formatter there? Have not tried it, hence guessing.

    //Daniel

  16. Configuration: yup that is correct!

  17. Matt Johnson says:

    Hi Henrik.  Thanks for calling out the Date issues.  This is one of the main reasons to use a different serializer like Json.Net or even ServiceStack.Text.  The ISO 8601 format solves lots of issues, and DataContractJsonSerializer is buggy even in its own implementation  (see connect.microsoft.com/…/723368)

    I appreciate the detail of your post!

    Cheers,

    Matt

  18. James says:

    It's not clear how you would integrate the JsonNetFormatter with a stand-alone HttpClient; how do you register alternative MediaTypeFormatter implementations with HttpClient; hopefully not via a HttpSelfHostConfiguration since that is just for hosting.

  19. James, there are overloads for passing in the formatters on the client side so that's how you can enable your own formatter(s).

  20. Aaron Williams says:

    I was trying to add your JsonNetFormatter class in a non-self hosted environment. I created an ASP.NET Web API Project. I added JsonNetFormatter in Global.asax.cs as such:

          static void Configure(HttpConfiguration config)

          {          

               // Create Json.Net formatter serializing DateTime using the ISO 8601 format

               JsonSerializerSettings serializerSettings = new JsonSerializerSettings();

               serializerSettings.Converters.Add(new IsoDateTimeConverter());

               config.Formatters.Add(new JsonNetFormatter(serializerSettings));

           }

           protected void Application_Start()

           {            

               AreaRegistration.RegisterAllAreas();

               RegisterGlobalFilters(GlobalFilters.Filters);

               RegisterRoutes(RouteTable.Routes);

               BundleTable.Bundles.RegisterTemplateBundles();

               Configure(GlobalConfiguration.Configuration);

           }

    I found that JsonNetFormatter OnWriteToStreamAsync() was never getting called. I learned the problem was that there already was a formatter for application/xml that was being used instead. So I had to remove that formatter and JsonNetFormatter was then being used. To remove the formatter I added the following at the beginning of the Configure method:

    var formatter = config.Formatters

               .Where(f =>

               {

                   return f.SupportedMediaTypes.Any(v => v.MediaType.Equals("application/json",                StringComparison.CurrentCultureIgnoreCase));

               })

               .FirstOrDefault();

               if (formatter != null)

               {

                   config.Formatters.Remove(formatter);

               }

    Just thought I would add this comment in case anyone else is trying the same.

  21. POST datetim says:

    Unexpected token parsing date. Expected String, got Date.

    apicontroller

           public Contact PostContact(Contact value)

           {

               _contacts.Add(value);

               return value;

           }

    model.

    public class Contact

       {

           public Contact()

           {

               LastModified = DateTime.Now;

           }

           public int Id { get; set; }

           public string Name { get; set; }

           public string Phone { get; set; }

           public string Email { get; set; }

           public DateTime LastModified { get; set; }

       }

  22. Jobzky says:

    Hi as Aaron Williams noted JSON.NET dll is no longer needed in order to output JSON as default instead of xml.

    1. Create MVC4 Web Api Project.

    2. Modify Global.asax. Add this method. This will remove the xml formatter

    static void Configure(HttpConfiguration config)

           {

               var formatter = config.Formatters.FirstOrDefault(f => f.SupportedMediaTypes.Any(v => v.MediaType.Equals("application/xml", StringComparison.CurrentCultureIgnoreCase)));

               if (formatter != null)

               {

                   config.Formatters.Remove(formatter);

               }

           }

    3. Call the above method from  Application_Start()

    Configure(GlobalConfiguration.Configuration);

    That's all. The API will give you json. I hope this will help others too

  23. Tiendq says:

    Using formatters[0] instead of formatters.Add to override default JSON formatter.

    JSON.NET is needed for better DateTime format.

  24. Using JSON.NET works well with DateTime

    Output correct ISO8601

    Input Dates now works well – 2012-03-15T09:21:59.2630521Z end up as a Datetime.Kind=Utc instead of local great.

    But my RouteParameter Id don't work now id is always  null

    api/client/100

    Did I miss something ?

  25. Giles says:

    would this be easy to do while debugging in visual studio?

  26. Terence says:

    Please see this comment on a Stackoverflow post: stackoverflow.com/…/9681171. While JSON.NET solves the date issues, it appears that it's incompatible with upshot.js / DbDataContext, and so POSTing new records breaks. Any suggested workarounds?

  27. Hi Henrik,

    I used the same custom formatter. When I called API a file is getting downloaded which has the serialised output.

    Thanks

  28. smitty25 says:

    Per the Web API roadmap – aspnetwebstack.codeplex.com/wikipage

    It looks like JSON.NET will be the defacto serialization method when Web API goes into release. Looking forward to this.

  29. Diana G says:

    Hi Henrik,

    I just translate your code to MVC4 WebAPI, it is great and it works. I was worried because I have to work with entities, and I couldn't fit IsReference=True to False, and this code solve my problem.

    Thank you very much.

  30. john papa says:

    great post. Love to see what changes are required with ASP.NET MVC 4 RC

  31. leik says:

    Seems like that in RC version your code have some troubles. Some classes disappeared from System.Net.Http.Formatting . Can you suggest me how to fix?

  32. anon says:

    any updates on this to use with the RC version?

  33. karthik says:

    Awesome article very much help full, I have got date serialized as 2012-02-18T00:54:06.8447642Z. Is there any way to get the date in specified format like 2012/02/08 etc. thanks in advance.

  34. SlavD says:

    anyone knows where do they keep IKeyValueModel in the release version?

  35. Art says:

    There is no IKeyValueModel and FormatterContext and etc. Seems to be copypaste from some of your project.