1

I have a JSON resulting from a mix of system data and user entries, like this :

{
    "Properties": [{
        "Type": "A",
        "Name": "aaa",
        "lorem ipsum": 7.1
    }, {
        "Type": "B",
        "Name": "bbb",
        "sit amet": "XYZ"
    }, {
        "Type": "C",
        "Name": "ccc",
        "abcd": false
    }]
}

I need to load it, process it, and save it to MongoDB. I deserialize it to this class :

public class EntityProperty {

    public string Name { get; set; }

    [JsonExtensionData]
    public IDictionary<string, JToken> OtherProperties { get; set; }

    public string Type { get; set; }
}

The problem is that MongoDB does not allow dots in key names, but the users can do whatever they want.

So I need a way to save this additional JSON data but I also need to change the key name as it's being processed.

I tried to add [JsonConverter(typeof(CustomValuesConverter))] to the OtherProperties attribute but it seems to ignore it.

Update/Clarification: since the serialization is done by Mongo (I send the objects to the library), I need the extension data names to be fixed during deserialization.

dbc
  • 104,963
  • 20
  • 228
  • 340
thomasb
  • 5,816
  • 10
  • 57
  • 92

1 Answers1

2

Update

Since the fixing of names must be done during deserialization, you could generalize the LowerCasePropertyNameJsonReader from How to change all keys to lowercase when parsing JSON to a JToken by Brian Rogers to perform the necessary transformation.

First, define the following:

public class PropertyNameMappingJsonReader : JsonTextReader
{
    readonly Func<string, string> nameMapper;

    public PropertyNameMappingJsonReader(TextReader textReader, Func<string, string> nameMapper)
        : base(textReader)
    {
        if (nameMapper == null)
            throw new ArgumentNullException();
        this.nameMapper = nameMapper;
    }

    public override object Value
    {
        get
        {
            if (TokenType == JsonToken.PropertyName)
                return nameMapper((string)base.Value);
            return base.Value;
        }
    }
}

public static class JsonExtensions
{
    public static T DeserializeObject<T>(string json, Func<string, string> nameMapper, JsonSerializerSettings settings = null)
    {
        using (var textReader = new StringReader(json))
        using (var jsonReader = new PropertyNameMappingJsonReader(textReader, nameMapper))
        {
            return JsonSerializer.CreateDefault(settings).Deserialize<T>(jsonReader);
        }
    }
}

Then deserialize as follows:

 var root = JsonExtensions.DeserializeObject<RootObject>(json, (s) => s.Replace(".", ""));

Or, if you are deserializing from a Stream via a StreamReader you can construct your PropertyNameMappingJsonReader directly from it.

Sample fiddle.

Alternatively, you could also fix the extension data in an [OnDeserialized] callback, but I think this solution is neater because it avoids adding logic to the objects themselves.

Original Answer

Assuming you are using Json.NET 10.0.1 or later, you can create your own custom NamingStrategy, override NamingStrategy.GetExtensionDataName(), and implement the necessary fix.

First, define MongoExtensionDataSettingsNamingStrategy as follows:

public class MongoExtensionDataSettingsNamingStrategy : DefaultNamingStrategy
{
    public MongoExtensionDataSettingsNamingStrategy()
        : base()
    {
        this.ProcessExtensionDataNames = true;
    }

    protected string FixName(string name)
    {
        return name.Replace(".", "");
    }

    public override string GetExtensionDataName(string name)
    {
        if (!ProcessExtensionDataNames)
        {
            return name;
        }
        return name.Replace(".", "");
    }
}

Then serialize your root object as follows:

var settings = new JsonSerializerSettings
{
    ContractResolver = new DefaultContractResolver { NamingStrategy = new MongoExtensionDataSettingsNamingStrategy() },
};
var outputJson = JsonConvert.SerializeObject(root, settings);

Notes:

  • Here I am inheriting from DefaultNamingStrategy but you could inherit from CamelCaseNamingStrategy if you prefer.

  • The naming strategy is only invoked to remap extension data names (and dictionary keys) during serialization, not deserialization.

  • You may want to cache the contract resolver for best performance.

  • There is no built-in attribute to specify a converter for dictionary keys, as noted in this question. And in any event Json.NET would not use the JsonConverter applied to OtherProperties since the presence of the JsonExtensionData attribute supersedes the converter property.

Alternatively, if it would be more convenient to specify the naming strategy using Json.NET serialization attributes, you will need a slightly different naming strategy. First create:

public class MongoExtensionDataAttributeNamingStrategy : MongoExtensionDataSettingsNamingStrategy
{
    public MongoExtensionDataAttributeNamingStrategy()
        : base()
    {
        this.ProcessDictionaryKeys = true;
    }

    public override string GetDictionaryKey(string key)
    {
        if (!ProcessDictionaryKeys)
        {
            return key;
        }
        return FixName(key);
    }        
}

And modify EntityProperty as follows:

[JsonObject(NamingStrategyType = typeof(MongoExtensionDataAttributeNamingStrategy))]
public class EntityProperty
{
    public string Name { get; set; }

    [JsonExtensionData]
    public IDictionary<string, JToken> OtherProperties { get; set; }

    public string Type { get; set; }
}

The reason for the inconsistency is that, as of Json.NET 10.0.3, DefaultContractResolver uses GetDictionaryKey() when remapping extension data names using a naming strategy that is set via attributes here, but uses GetExtensionDataName() when the naming strategy is set via settings here. I have no explanation for the inconsistency; it feels like a bug.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • It's a very good answer, but unfortunately it doesn't solve my problem... since the serialization is done by Mongo (I send the objects to the library), and your solution works on **de**serialization... For now I have solved it with poorly written `JsonConverter`s but I would love to learn of a better solution, your "naming strategy" feels much nicer. – thomasb Nov 29 '17 at 16:47
  • @thomasb - answer updated using new information. – dbc Nov 29 '17 at 23:35
  • Oh my. I can't upvote twice ! :D It works perfectly, thanks ! But now my question is a duplicate... I'll mark it as such. – thomasb Nov 30 '17 at 08:52