FYI: Custom Json formatter

Topics: Web Api
Oct 25, 2011 at 7:33 AM

Hi, I've had some serious problems getting our existing json formatter to work with preview 5. Our custom formatter adds metadata as a header in the json http content and also uses JSON.NET to serialize itself.

Apparently; I can only get it to work if I let it inherit from JsonMediaTypeFormatter. If it just inherits from MediaTypeFormatter the OnWriteToStream is not being called even though the formatter is added, like it was before, in the global.asax. 

If I, however, don't do the same for my custom Xml formatter (so let it just inherit from MediaTypeFormatter), it is being called. Unfortunately, I can't let my xml formatter inherit from XmlMediaTypeFormatter because then it starts complaining that not all return types are serializable.

Is this desired behaviour?

Oct 25, 2011 at 1:51 PM
Edited Oct 25, 2011 at 2:01 PM

Hi,

Is it possible for you to give a small snapshot of your custom formatter code and as well as the way you are adding the formatter?

My guess is that your custom media type formatter is being added at the end of the formatter list (after the 3 default formatters that WebAPI ships with: Xml, Json, JsonValue). When trying to select  a formatter to write, we select the first match(based on media type mappings) which would be the default WebAPI's JsonMediaTypeFormatter...hence, your custom media type formatter is not getting selected...you can try placing your formatter before the default json media type formatter and see if it works...

But I would be surprised by the behavior that you mentioned for the custom xml media type formatter though...

A snapshot of code would really help us in quickly diagnosing the problem...can you please send this?

thanks,Kiran Challa

Oct 25, 2011 at 3:22 PM

Hi Kiran, thanks for replying, here it it. We clean up the formatters which are default though.

Off-topic: I can understand the decision to add default formatters, but I would decide against that if I would be rolling out an API. Seems like one of those things where you would like the user to be aware of which behavior is added rather than supplying defaults. This is though, I feel, largely a matter of opinion.

Anyway, this is my global.asax:

            WebApiConfiguration wac = new WebApiConfiguration
            {
                EnableTestClient = true,
                EnableHelpPage = true,

                RequestHandlers = (requestHandlers, serviceEndpoint, operationDescriptions) =>
                {
                    requestHandlers.Add(new SecurityOperationHandler());
                },

                ErrorHandlers = (errorHandlers, serviceEndpoint, operationDescriptions) =>
                {
                    //errorHandlers.Add(new GlobalErrorHandler());
                }
            };

            RouteTable.Routes.SetDefaultHttpConfiguration(wac);
            
            // AutoRedirect if the uri ends with a slash ('/').
            wac.TrailingSlashMode = TrailingSlashMode.AutoRedirect;

            wac.Formatters.Clear();
            wac.Formatters.Add(new ADPFormUrlEncodedFormatter());
            wac.Formatters.Add(new ADPJSONFormatter());
            wac.Formatters.Add(new ADPXmlFormatter());
            wac.Formatters.Add(new ADPImageFormatter());
            
            wac.MessageHandlers.Add(typeof(LoggingHandler));
            wac.MessageHandlers.Add(typeof(SecurityHandler));

            wac.MaxReceivedMessageSize = 1024 * 1024 * 10;  // Allow files up to 10MB.
            wac.MaxBufferSize = 1024 * 1024 * 10;
            
            // Instead of using a custom error handler, we can turn on the IncludeExceptionDetail if we want to see the default web browser page with the exception call stack.
            wac.IncludeExceptionDetail = true;

            // NOTE: PLEASE use proper casing in the prefixes! So not like this 'employeestore' or this 'EMPLOYEESTORE' but like this 'EmployeeStore'.
            // For now, it has to be the same as the class names in javascript. i.e. 'var datastore = new hrvolution.proxy.EmployeeStore();'
            // If in the future we think of something better, remove this comment.
            AddToRouteTable<ADP.Perman.WebServices.REST.EmployeeStoreContext.Service>("EmployeeStore");
            AddToRouteTable<ADP.Perman.WebServices.REST.CustomerStoreContext.Service>("CustomerStore");
            AddToRouteTable<ADP.Perman.WebServices.REST.CustomerListStoreContext.CustomerListService>("CustomerListStore");
            AddToRouteTable<ADP.Perman.WebServices.REST.EmployeeListStoreContext.Service>("EmployeeListStore");
            AddToRouteTable<SecurityStoreService>("SecurityStore");
            AddToRouteTable<PayPeriodListService>("PayPeriodListStore");
            AddToRouteTable<PayPeriodService>("PayPeriodStore");

This is our JSON formatter (we use JSON.NET):

    public class ADPJSONFormatter : JsonMediaTypeFormatter
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ADPJSONFormatter"/> class.
        /// </summary>
        public ADPJSONFormatter()
        {
            this.AddUriPathExtensionMapping("json", "application/json");
            this.AddQueryStringMapping("format", "json", "application/json");

            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json"));
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
        }

        // snip ...

Any input on my code is really appreciated. The project we've decide to use it for is hugely important to our company and is to be released April 2012. Any flaws in the way I interpret the bits you release I'd really like to have pointed out to me.

Gerben.

Oct 25, 2011 at 8:15 PM

This definitely looks like a bug in Preview 5.  I can't get my custom JsonNetMediaTypeFormatter to work either unless it inherits from JsonMediaTypeFormatter. Same problem as stated in the original post.

It's not an order problem as I'm clearing all default formatters and having this issue.

    /// <summary>
    /// Defines a Json <see cref="MediaTypeFormatter"/> using Json.NET serialization/deserialization.
    /// </summary>
    public class JsonNetMediaTypeFormatter2 : MediaTypeFormatter
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="JsonNetMediaTypeFormatter"/> class.
        /// </summary>
        public JsonNetMediaTypeFormatter2()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json") { CharSet = "utf-8" });
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/json") { CharSet = "utf-8" });
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/bson") { CharSet = "utf-8" });
        }
        
        /// <summary>
        /// Called when [read from stream].
        /// </summary>
        /// <param name="type">The type of object to deserialize.</param>
        /// <param name="stream">The stream.</param>
        /// <param name="httpContentHeaders">The HTTP content headers.</param>
        /// <returns>The de-serialized object.</returns>
        /// <remarks></remarks>
        protected override Object OnReadFromStream(Type type, Stream stream, HttpContentHeaders httpContentHeaders)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            return httpContentHeaders.ContentType.MediaType.ToLower() == "application/bson" 
                ? stream.ReadAsBsonSerializable(type) 
                : stream.ReadAsJsonSerializable(type);
        }

        /// <summary>
        /// Called to write an object to the <paramref name="stream"/>.
        /// </summary>
        /// <param name="type">The type of object to write.</param>
        /// <param name="value">The object instance to write.</param>
        /// <param name="stream">The <see cref="T:System.IO.Stream"/> to which to write.</param>
        /// <param name="httpContentHeaders">The HTTP content headers.</param>
        /// <param name="context">The <see cref="T:System.Net.TransportContext"/>.</param>
        /// <remarks></remarks>
        protected override void OnWriteToStream(Type type, Object value, Stream stream, HttpContentHeaders httpContentHeaders, TransportContext context)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }

            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            if (httpContentHeaders.ContentType.MediaType.ToLower() == "application/bson")
            {
                stream.WriteAsBsonSerializable(value);
            }
            else
            {
                stream.WriteAsJsonSerializable(value);
            }
        }
    }

Coordinator
Oct 25, 2011 at 9:06 PM

Yes, this is a known issue in Preview 5. A fix has been checked into the default repository.

Daniel Roth

Oct 25, 2011 at 10:28 PM

Thanks Dan ... I appreciate the update.

Coordinator
Oct 25, 2011 at 11:42 PM

Gerben, have you considered creating your own configuration derived from WebConfiguration in order to package up your config code rather than inlining it? That would allow you to reuse that config class across different applications and might help clean up your code a bit.

Just a thought...

Glenn

Oct 26, 2011 at 5:48 AM

Hi Glenn,

You're right, that's something I'm absolutely doing. I'm trying to follow you guys around though (in a good way :) so I'm hopping from preview to preview. I don't know precisely what will change in the time to come so I'll probably leave the big refactor / cleanup for when you are in, say, beta 2 or something.

Oct 26, 2011 at 11:33 AM
Edited Oct 26, 2011 at 11:46 AM

Nice,glad that I found a solution.

But where can i find this fix? Do I just need to download the latest source and build this locally?

Oct 26, 2011 at 2:04 PM
kiranchalla wrote:

Hi,

Is it possible for you to give a small snapshot of your custom formatter code and as well as the way you are adding the formatter?

My guess is that your custom media type formatter is being added at the end of the formatter list (after the 3 default formatters that WebAPI ships with: Xml, Json, JsonValue). When trying to select  a formatter to write, we select the first match(based on media type mappings) which would be the default WebAPI's JsonMediaTypeFormatter...hence, your custom media type formatter is not getting selected...you can try placing your formatter before the default json media type formatter and see if it works...

But I would be surprised by the behavior that you mentioned for the custom xml media type formatter though...

A snapshot of code would really help us in quickly diagnosing the problem...can you please send this?

thanks,Kiran Challa

Hi Kiran,

How do I place my formatter before the default ones? I too have a custom formatter (inherited from MediaTypeFormatter) for Xml and Json but it is not being called even though i have tried to insert the formatter at index 0.

Oct 26, 2011 at 2:04 PM
danroth27 wrote:

Yes, this is a known issue in Preview 5. A fix has been checked into the default repository.

 

Daniel Roth

Hi Dan,

 

How do we get the fix?

Thanks,

Ashwin Patti

Oct 26, 2011 at 3:11 PM

Hi,

 

The following method worked for me:

var config = new WebApiConfiguration()
                    {
                        EnableHelpPage=true,
                        EnableTestClient=true,
                        TrailingSlashMode=TrailingSlashMode.Ignore,
                    };
RouteTable.Routes.SetDefaultHttpConfiguration(config);

var xmlformatter = new XmlMediaTypeFormatter();
xmlformatter.SupportedMediaTypes.Clear();
var jsonformatter = new JsonMediaTypeFormatter();
jsonformatter.SupportedMediaTypes.Clear();

config.Formatters.Clear();
config.Formatters.AddRange(xmlformatter,jsonformatter, new MediaFormatter());

//MediaFormatter is my custom formatter class.

By doing above, i was able to get custom formatter working.

Ashwin Patti

Coordinator
Oct 26, 2011 at 4:37 PM

Yes, you will have to do a local build until we get an updated NuGet package published.

Daniel Roth

Oct 26, 2011 at 4:41 PM
danroth27 wrote:

Yes, you will have to do a local build until we get an updated NuGet package published.

 

Daniel Roth

Any ETA on preview 6? *wink wink nudge nudge*  And can you please make it CLS compliant and signed with a SNK? pretty please? :)

Coordinator
Oct 26, 2011 at 6:02 PM

Nothing to announce yet in terms of timeframe.

Please vote for these Issue Tracker issues though if you care deeply about CLS Compliance and strong name signing:

http://wcf.codeplex.com/workitem/65

http://wcf.codeplex.com/workitem/101

Daniel Roth

Oct 28, 2011 at 4:49 PM

Daniel, do you mind indicating in which changeset this issue was resolved?  I couldn't get a sense from the commit messages and I'm having trouble matching up all the referenced assemblies doing a local build (has MapServiceRoute changed or moved?).

Thank you.

Coordinator
Oct 28, 2011 at 6:56 PM

Sorry about the lack of clarity in the check-in messages. We are working to clean-up our processes for getting our code out to the CodePlex repository.

The fix should be in this changeset: http://wcf.codeplex.com/SourceControl/changeset/changes/2ec878569897

If you are using the Web API Enhancements (like MapServiceRoute<T>) then you will need to do a local build from the prototypes branch.

Daniel Roth