7

I'm looking to write a JsonConverter which escapes HTML in strings, unless the [AllowHtml] attribute has been applied;

    private class ObjectWithStrings
    {
        // will be HTML-escaped
        public string Name { get; set; }

        // won't be escaped
        [AllowHtml]
        public string Unsafe { get; set; }
    }

So I'm trying to write a JsonConverter with a custom ReadJson property;

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var s = (string)reader.Value;
        if (s == null)
        {
            return null;
        }

        // here I need to get a PropertyInfo so I can call GetCustomAttribute<AllowHtmlAttribute>();

        var encoded = System.Web.Security.AntiXss.AntiXssEncoder.HtmlEncode(s, useNamedEntities: true);
        return encoded;
    }

The gap I've got is that I can't see if Json.Net will let me know the property I'm reading into. Consequently, I can't figure out how to get the property's custom attributes.

Is there a way to find out what property I'm serialising into, or a different pattern recommended for this kind of thing?

EDIT: I failed to write a clear question; I've attempted to write a JsonConverter which deserialises strings, -- see the implementation above of CanConvert(). I suspect that choice is the start of my problem; I may need to deserialise objects with string properties, and do a standard deserialize except when deserialising particular properties.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Steve Cooper
  • 20,542
  • 15
  • 71
  • 88
  • You have the type of object and the object itself. Why do you need to look for the property? You can simply check if a property has the custom attribite or not – Yuval Itzchakov Sep 14 '15 at 10:33
  • @YuvalItzchakov The type is completely the same for `Name` and `Unsafe`, is it not? They're both `string` properties of `ObjectWithStrings`. –  Sep 14 '15 at 10:34
  • I know this doesn't answer your question, but in my experience, in most cases, it's easier to solve this from the client. Instead of HTML-escaping `Name`, simply don't interpret `Name` as HTML. –  Sep 14 '15 at 10:35
  • The object type is `ObjectWithStrings`. `Name` and `Unsafe` are strings. Iterate each property and check for the attribute. – Yuval Itzchakov Sep 14 '15 at 10:35
  • @YuvalItzchakov If you iterate over all `string` properties of `ObjectWithStrings` (I'm not sure if you even get that much indication, but suppose you do), then you always find the `AllowHtml` attribute, since one of the properties has that attribute. For the `Name` property, it should *not* be found. That's the whole point of the question. –  Sep 14 '15 at 10:37
  • @hvd Not if you check each property for `Attribute.IsDefined`. You can filter only the `string` properties and check them and only them. – Yuval Itzchakov Sep 14 '15 at 10:39
  • @YuvalItzchakov If you think you have an answer, please post it as an answer. What you have in your comments here just doesn't make any sense, and I suspect that if you try to put it into a well-written answer, you will agree and not post it (or even better, find the missing bit that makes it work). :) –  Sep 14 '15 at 10:42
  • Sorry, all -- I realise my original question is misleading. I've actually defined a JsonConverter which converts strings, rather than objects with string properties. This is because I want to make sure that all my strings by default are escaped, except where I explicitly rule them in. This may be where my problem starts... – Steve Cooper Sep 14 '15 at 12:47

1 Answers1

7

From within a custom JsonConverter, you can find the name of the JSON property being deserialized by picking it out of the Path property from the JsonReader.

string propertyName = reader.Path.Split('.').Last();

However, this will not solve your overall problem. Assuming the name of the JSON property matches your target class property, you'd still need a way to get the parent object type so you can get the custom attributes from it. Unfortunately, this information is not available to you inside a converter. A converter is intended to be responsible only for the object type it says it can convert (string in your case), and that object's child properties (none in this case, since string is a primitive). So, to make it work, the converter would need to be written to operate on the parent class, and would then need to handle all the string properties of that class. Since your goal seems to be to apply the HTML encoding behavior to all strings in all classes, then you would need a generic converter that handles all non-primitive types, which could get pretty messy, depending on the breadth of what you're trying to deserialize.

Fortunately, there is a better way. Instead of using a JsonConverter, you can use a custom IContractResolver in combination with a IValueProvider to solve this. A ContractResolver is much better suited to problems like this where you want to apply a certain behavior broadly.

Below is an example of the code you would need. The CustomResolver class extends the DefaultContractResolver provided by Json.Net. The CreateProperties() method inspects the JsonProperty objects created by the base resolver and attaches an instance of the inner HtmlEncodingValueProvider class to any string properties which do not have the [AllowHtml] attribute applied. Each value provider later handles the actual encoding of its target string property via the SetValue() method.

public class CustomResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);

        // Find all string properties that do not have an [AllowHtml] attribute applied
        // and attach an HtmlEncodingValueProvider instance to them
        foreach (JsonProperty prop in props.Where(p => p.PropertyType == typeof(string)))
        {
            PropertyInfo pi = type.GetProperty(prop.UnderlyingName);
            if (pi != null && pi.GetCustomAttribute(typeof(AllowHtmlAttribute), true) == null)
            {
                prop.ValueProvider = new HtmlEncodingValueProvider(pi);
            }
        }

        return props;
    }

    protected class HtmlEncodingValueProvider : IValueProvider
    {
        PropertyInfo targetProperty;

        public HtmlEncodingValueProvider(PropertyInfo targetProperty)
        {
            this.targetProperty = targetProperty;
        }

        // SetValue gets called by Json.Net during deserialization.
        // The value parameter has the original value read from the JSON;
        // target is the object on which to set the value.
        public void SetValue(object target, object value)
        {
            var encoded = System.Web.Security.AntiXss.AntiXssEncoder.HtmlEncode((string)value, useNamedEntities: true);
            targetProperty.SetValue(target, encoded);
        }

        // GetValue is called by Json.Net during serialization.
        // The target parameter has the object from which to read the string;
        // the return value is the string that gets written to the JSON
        public object GetValue(object target)
        {
            // if you need special handling for serialization, add it here
            return targetProperty.GetValue(target);
        }
    }
}

To use the resolver, create a new JsonSerializerSettings instance, then set its ContractResolver property to a new instance of the custom resolver and pass the settings to the JsonConvert.DeserializeObject() method.

Here is a short demo:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        { 
            ""Name"" : ""<b>Foo Bar</b>"", 
            ""Description"" : ""<p>Bada Boom Bada Bing</p>"", 
        }";

        JsonSerializerSettings settings = new JsonSerializerSettings
        {
            ContractResolver = new CustomResolver()
        };

        Foo foo = JsonConvert.DeserializeObject<Foo>(json, settings);
        Console.WriteLine("Name: " + foo.Name);
        Console.WriteLine("Desc: " + foo.Description);
    }
}

class Foo
{
    public string Name { get; set; }
    [AllowHtml]
    public string Description { get; set; }
}

class AllowHtmlAttribute : Attribute { }

Here is the output. Notice that the Name property gets HTML encoded while the Description property does not.

Name: &lt;b&gt;Foo Bar&lt;/b&gt;
Desc: <p>Bada Boom Bada Bing</p>

Fiddle: https://dotnetfiddle.net/cAg4NC

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300