1

I am using NLog, and am sending a LogEventInfo object across the wire, I do not own this object and therefore I can't decorate it with [JsonConverter(typeof(StringEnumConverter))], or maybe I can and I just don't know how

I've tried the following code and it doesn't help

var strategy = new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy();
var sec = new StringEnumConverter(strategy, false);

//TEMP Hard code serialization and message routing for just logs
LogEventInfo info = Newtonsoft.Json.JsonConvert.DeserializeObject<LogEventInfo>(msg.Body, sec);

Does anyone have any luck getting enums to deserialize and the enum is in a class you don't own?

Here is the message:

{
   "date":"2019-06-04 21:48:24.0753",
   "level":"Error",
   "message":"{\"ApplicationId\":1390760,\"AppStatus\":\"#PG2\",\"Status\":400,\"ErrorCode\": 1053 }",
   "properties":"ResponseBody={\"ApplicationId\":1390760,\"AppStatus\":\"#PG2\",\"Status\":400,\"ErrorCode\": 1053 }|GroupId=392934|ApplicationId=1390760|Status=400",
   "callsite":"HFD.Enterprise.Logging.Tests.LogTest.LoadTest",
   "logger":"WebApiLog",
   "machinename":"BRANDONHOSTVS"
}

And here is the exception:

Newtonsoft.Json.JsonSerializationException: Error converting value "Error" to type 'NLog.LogLevel'. Path 'level', line 1, position 72. ---> System.InvalidCastException: Invalid cast from 'System.String' to 'NLog.LogLevel'.
   at System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
   at System.String.System.IConvertible.ToType(Type type, IFormatProvider provider)
   at System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.EnsureType(JsonReader reader, Object value, CultureInfo culture, JsonContract contract, Type targetType)
   --- End of inner exception stack trace ---
dbc
  • 104,963
  • 20
  • 228
  • 340
Brandon Slezak
  • 187
  • 1
  • 10
  • It looks as though `"message"` is an embedded double-serialized JSON string, and `"properties"` contains embedded JSON surrounded by some additional text. Could this be the problem? Even if you don't cannot modify the class can you share its reference source, i.e. a [mcve]? – dbc Jun 05 '19 at 05:29

1 Answers1

4

Your basic problem is that LogEventInfo.Level is not an enum, it is a class LogLevel that happens to implement IConvertible and have a set of global static standard values:

public sealed class LogLevel : IComparable, IEquatable<LogLevel>, IConvertible
{
    /// <summary>
    /// Gets all the available log levels (Trace, Debug, Info, Warn, Error, Fatal, Off).
    /// </summary>
    public static IEnumerable<LogLevel> AllLevels => allLevels;

Adding StringEnumConverter won't help you (de)serialize such a property.

What's more, it seems that NLog doesn't provide a custom TypeConverter for LogLevel. If I call TypeDescriptor.GetConverter(typeof(LogLevel)) the returned value is an instance of the default converter System.ComponentModel.TypeConverter, which means Json.NET has no way to convert a serialized LogLevel string value back into a LogLevel instance.

Nevertheless your JSON sample does represent Level as "level": "Error", and so the sending system must have been using some sort of custom JsonConverter for LogLevel that serializes just the LogLevel.Name. We can recreate such a JsonConverter quite easily as follows:

public class LogLevelConverter : JsonConverter<LogLevel>
{
    public override LogLevel ReadJson(JsonReader reader, Type objectType, LogLevel existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContentAndAssert().TokenType)
        {
            case JsonToken.Null:
                return null;
            case JsonToken.String:
                return LogLevel.FromString((string)reader.Value);
            default:
                throw new JsonSerializationException(string.Format("Unknown token {0}", reader.TokenType));
        }
    }

    public override void WriteJson(JsonWriter writer, LogLevel value, JsonSerializer serializer)
    {
        var logLevel = (LogLevel)value;
        writer.WriteValue(logLevel.Name);
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

And then use it as follows:

var seq = new LogLevelConverter();
var info = Newtonsoft.Json.JsonConvert.DeserializeObject<LogEventInfo>(msg.Body, seq);

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • "it seems that NLog doesn't provide a custom TypeConverter for LogLevel". How could NLog provide that? (PR likely accepted ;) – Julian Jun 06 '19 at 22:20
  • @Julian - just by adding some appropriate [`TypeConverterAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.typeconverterattribute) to `LogLevel`, e.g. `[TypeConverter(typeof(LogLevelTypeConverter))]`. See [c# how to implement type converter](https://stackoverflow.com/q/43649402). Having done that, Json.NET will pick it up, see [Json.Net: Serialize/Deserialize property as a value, not as an object](https://stackoverflow.com/q/40480489). – dbc Jun 06 '19 at 22:23
  • @Julian - however, the current implementation is a little odd. If I do `JsonConvert.SerializeObject(LogLevel.Info)` the result is `"2"`, apparently because `((IConvertible)LogLevel.Info).ToType(typeof(string), CultureInfo.InvariantCulture))` returns `2`. Adding in a `TypeConverter` would change this pre-existing (but possibly undesirable) behavior. See https://dotnetfiddle.net/3nzhLr – dbc Jun 06 '19 at 22:41
  • 2
    FYI a TypeConverter for this has been added to NLog 4.6.5 (released today) – Julian Jun 14 '19 at 01:15