static LogEvent ReadFromJObject(int lineNumber, JObject jObject)
        {
            var timestamp = GetRequiredTimestampField(lineNumber, jObject, ClefFields.Timestamp);

            string messageTemplate;

            if (!TryGetOptionalField(lineNumber, jObject, ClefFields.MessageTemplate, out messageTemplate))
            {
                string message;
                if (!TryGetOptionalField(lineNumber, jObject, ClefFields.Message, out message))
                {
                    throw new InvalidDataException($"The data on line {lineNumber} does not include the required `{ClefFields.MessageTemplate}` or `{ClefFields.Message}` field.");
                }

                messageTemplate = MessageTemplateSyntax.Escape(message);
            }

            var level = LogEventLevel.Information;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.Level, out string l))
            {
                level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel), l);
            }
            Exception exception = null;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.Exception, out string ex))
            {
                exception = new TextException(ex);
            }

            var unrecognized = jObject.Properties().Where(p => ClefFields.IsUnrecognized(p.Name));

            // ReSharper disable once PossibleMultipleEnumeration
            if (unrecognized.Any())
            {
                // ReSharper disable once PossibleMultipleEnumeration
                var names = string.Join(", ", unrecognized.Select(p => $"`{p.Name}`"));
                throw new InvalidDataException($"{names} on line {lineNumber} are unrecognized.");
            }

            var parsedTemplate = Parser.Parse(messageTemplate);
            var renderings     = Enumerable.Empty <Rendering>();

            if (jObject.TryGetValue(ClefFields.Renderings, out JToken r))
            {
                var renderedByIndex = r as JArray;
                if (renderedByIndex == null)
                {
                    throw new InvalidDataException($"The `{ClefFields.Renderings}` value on line {lineNumber} is not an array as expected.");
                }

                renderings = parsedTemplate.Tokens
                             .OfType <PropertyToken>()
                             .Where(t => t.Format != null)
                             .Zip(renderedByIndex, (t, rd) => new Rendering(t.PropertyName, t.Format, rd.Value <string>()))
                             .ToArray();
            }

            var properties = jObject
                             .Properties()
                             .Where(f => !ClefFields.All.Contains(f.Name))
                             .Select(f =>
            {
                var name = ClefFields.Unescape(f.Name);
                var renderingsByFormat = renderings.Where(rd => rd.Name == name);
                return(PropertyFactory.CreateProperty(name, f.Value, renderingsByFormat));
            })
                             .ToList();

            string eventId;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.EventId, out eventId)) // TODO; should support numeric ids.
            {
                properties.Add(new LogEventProperty("@i", new ScalarValue(eventId)));
            }

            return(new LogEvent(timestamp, level, exception, parsedTemplate, properties));
        }
        static LogEvent ReadFromJObject(int lineNumber, JObject jObject)
        {
            var timestamp = GetRequiredTimestampField(lineNumber, jObject, ClefFields.Timestamp);

            string messageTemplate;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.MessageTemplate, out var mt))
            {
                messageTemplate = mt;
            }
            else if (TryGetOptionalField(lineNumber, jObject, ClefFields.Message, out var m))
            {
                messageTemplate = MessageTemplateSyntax.Escape(m);
            }
            else
            {
                messageTemplate = null;
            }

            var level = LogEventLevel.Information;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.Level, out string l))
            {
                level = (LogEventLevel)Enum.Parse(typeof(LogEventLevel), l);
            }
            Exception exception = null;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.Exception, out string ex))
            {
                exception = new TextException(ex);
            }

            var parsedTemplate = messageTemplate == null ?
                                 new MessageTemplate(Enumerable.Empty <MessageTemplateToken>()) :
                                 Parser.Parse(messageTemplate);

            var renderings = Enumerable.Empty <Rendering>();

            if (jObject.TryGetValue(ClefFields.Renderings, out JToken r))
            {
                var renderedByIndex = r as JArray;
                if (renderedByIndex == null)
                {
                    throw new InvalidDataException($"The `{ClefFields.Renderings}` value on line {lineNumber} is not an array as expected.");
                }

                renderings = parsedTemplate.Tokens
                             .OfType <PropertyToken>()
                             .Where(t => t.Format != null)
                             .Zip(renderedByIndex, (t, rd) => new Rendering(t.PropertyName, t.Format, rd.Value <string>()))
                             .ToArray();
            }

            var properties = jObject
                             .Properties()
                             .Where(f => !ClefFields.All.Contains(f.Name))
                             .Select(f =>
            {
                var name = ClefFields.Unescape(f.Name);
                var renderingsByFormat = renderings.Where(rd => rd.Name == name);
                return(PropertyFactory.CreateProperty(name, f.Value, renderingsByFormat));
            })
                             .ToList();

            string eventId;

            if (TryGetOptionalField(lineNumber, jObject, ClefFields.EventId, out eventId)) // TODO; should support numeric ids.
            {
                properties.Add(new LogEventProperty("@i", new ScalarValue(eventId)));
            }

            return(new LogEvent(timestamp, level, exception, parsedTemplate, properties));
        }