6

I have a large arbitrary JSON structure as a JObject reference in my code.

I want to serialise this structure, except when I encounter a JObject containing a property called type with value "encrypted" then I want to remove the adjacent data property before writing the object.

In other words, if I encounter this:

{
  type: "encrypted",
  name: "some-name",
  data: "<base64-string>"
}

It will be serialized as this:

{
  type: "encrypted",
  name: "some-name"
}

I can't mutate the structure, and cloning it before mutating would be too inefficient, so I tried using a JsonConverter as follows:

public class RemoveEncryptedDataSerializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(JObject);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var o = (JObject)value;
        if (o.Value<string>("type") != "encrypted")
        {
            o.WriteTo(writer);
            return;
        }

        var copy = o.DeepClone();
        copy["data"]?.Parent.Remove();
        copy.WriteTo(writer);
    }
}

However the CanConvert function only seems to be called with types not derived from JToken, so my WriteJson function is never called.

Is there another way to achieve this?


Edit: Here is some code you can use for testing:

[TestMethod]
public void ItShouldExcludeEncryptedData()
{
    var input = JObject.Parse(@"
    {
        a: {
            type: 'encrypted',
            name: 'some-name',
            data: 'some-data'
        }
    }");

    var expected = JObject.Parse(@"
    {
        a: {
            type: 'encrypted',
            name: 'some-name',
        }
    }");

    var output = input.ToString(Formatting.Indented, new RemoveEncryptedDataSerializer());

    Assert.AreEqual(
        expected.ToString(Formatting.Indented),
        output);
}
3
  • try to return true; instead of return objectType == typeof(JObject); in your CanConvert method and let me know it it works or not. Commented Dec 7, 2018 at 13:14
  • also try to add this method to your converter => public override bool CanRead { get { return false; } } Commented Dec 7, 2018 at 13:17
  • Thanks @er-shoaib, unfortunately this does not work as WriteJson is still only called for the leave nodes of the structure. Commented Dec 7, 2018 at 14:41

2 Answers 2

2

The converter needs to be built to handle a JToken and it must work recursively to ensure all the encrypted data is redacted.

I was able to get the following converter to work:

public class RemoveEncryptedDataConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(JToken).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JToken token = (JToken)value;
        if (token.Type == JTokenType.Object)
        {
            bool omitDataProperty = token.Value<string>("type") == "encrypted";

            writer.WriteStartObject();
            foreach (var prop in token.Children<JProperty>())
            {
                if (omitDataProperty && prop.Name == "data")
                    continue;

                writer.WritePropertyName(prop.Name);
                serializer.Serialize(writer, prop.Value);  // recurse
            }
            writer.WriteEndObject();
        }
        else if (token.Type == JTokenType.Array)
        {
            writer.WriteStartArray();
            foreach (var item in token.Children())
            {
                serializer.Serialize(writer, item);  // recurse
            }
            writer.WriteEndArray();
        }
        else // JValue
        {
            token.WriteTo(writer);
        }
    }
}

Working demo: https://dotnetfiddle.net/0K61Bz


If you want to work with a JsonWriter directly through a stream, you can refactor the logic from the converter into a recursive extension method and use that. You don't need a converter if you're not using the serializer.

public static class JsonExtensions
{
    public static void RedactedWriteTo(this JToken token, JsonWriter writer)
    {
        if (token.Type == JTokenType.Object)
        {
            bool omitDataProperty = token.Value<string>("type") == "encrypted";

            writer.WriteStartObject();
            foreach (var prop in token.Children<JProperty>())
            {
                if (omitDataProperty && prop.Name == "data")
                    continue;

                writer.WritePropertyName(prop.Name);
                prop.Value.RedactedWriteTo(writer);  // recurse
            }
            writer.WriteEndObject();
        }
        else if (token.Type == JTokenType.Array)
        {
            writer.WriteStartArray();
            foreach (var item in token.Children())
            {
                item.RedactedWriteTo(writer);  // recurse
            }
            writer.WriteEndArray();
        }
        else // JValue
        {
            token.WriteTo(writer);
        }
    }
}

Then you can use it like this, where stream is your output stream and input is your JObject:

using (StreamWriter sw = new StreamWriter(stream))  // or StringWriter if you prefer
using (JsonWriter writer = new JsonTextWriter(sw))
{
    writer.Formatting = Formatting.Indented;
    input.RedactedWriteTo(writer);
}

Fiddle: https://dotnetfiddle.net/l949HU

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks Brian, that's really helpful! Interestingly, my test still failed until I changed from using ToString(...) to using JsonConvert.SerializeObject(...). In reality I need to write to a stream, but WriteTo(...) also fails in the same way as ToString(...). I'm a bit surprised they produce a different output, but this reproduces it: dotnetfiddle.net/9lkFJO . Do you know how I could write to a stream while getting the same output as JsonConvert.SerializeObject?
@JamesThurley Updated my answer to show the approach for a stream.
0

Assuming you are using Newtonsoft JSON.Net library;

To conditionally serialize a property, add a method that returns boolean with the same name as the property and then prefix the method name with ShouldSerialize. The result of the method determines whether the property is serialized. If the method returns true then the property will be serialized, if it returns false then the property will be skipped.

For your example:

public class EncryptedData
{
    public string Type { get; set; }
    public string Name { get; set; }
    public string Data { get; set; }

    public bool ShouldSerializeData()
    {
        // don't serialize the Data property if the Type equals "encrypted"
        return (Type != "encrypted");
    }
}

1 Comment

Thanks, this is a useful tip, however in this case the JSON is an arbitrary structure and so I cannot deserialize it to a concrete type.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.