Исключение JSON.NET при десериализации значения DateTime

Я отразил код класса JSON.NET JavaScriptDateTimeConverter, скопировал его и переименовал класс AS3DateTimeConverter, чтобы я мог изменить его для форматирования объектов DateTime в более точном и строго типизированном поместье.

У меня он выводит тип в соответствии с тем, как JSON.NET выводит строго типизированные объекты, например: {"$type":"System.DateTime, mscorlib","ticks":0}

Переопределенный метод WriteJson JsonConverter запускается для получения этого значения.

Однако, когда я пытаюсь десериализовать строку, используя те же самые настройки с тем же конвертером, переопределенный метод ReadJson никогда не получает возможности запустить и построить DateTime из свойства ticks, потому что возникают следующие ошибки:

Невозможно десериализовать текущий объект JSON (например, {"name": "value"}) в тип 'System.DateTime', поскольку для правильной десериализации типу требуется примитивное значение JSON (например, строка, число, логическое значение, null).

Чтобы исправить эту ошибку, либо измените JSON на примитивное значение JSON (например, строку, число, логическое значение, null), либо измените десериализованный тип, чтобы он был обычным типом .NET (например, не примитивным типом, таким как целое число, а не типом коллекции как массив или список), который можно десериализовать из объекта JSON. JsonObjectAttribute также можно добавить к типу, чтобы заставить его десериализоваться из объекта JSON.

Путь 'галочки', строка 1, позиция 45.

Это какая-то ошибка или ограничение, которое не позволяет мне оживить тип DateTime, потому что это тип значения? Или я что-то упускаю?

Вот настройки сериализации:

    JsonSerializerSettings settings = new JsonSerializerSettings();
    settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
    settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
    settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
    settings.ConstructorHandling = ConstructorHandling.Default;
    settings.TypeNameHandling = TypeNameHandling.All;
    settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
    settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
    settings.DateParseHandling = DateParseHandling.DateTime;
    settings.Converters.Add( new AS3DateTimeConverter() );
    //settings.Binder = new AS3SerializationBinder();
    string s = JsonConvert.SerializeObject( new DateTime( 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ), settings );
    object o = JsonConvert.DeserializeObject( s, settings ); //s = "{\"$type\":\"System.DateTime, mscorlib\",\"ticks\":0}" //ERROR OCCURS HERE

person Triynko    schedule 26.11.2013    source источник
comment
На самом деле он не будет хранить тики, он будет хранить миллисекунды, прошедшие с 1 января 1970 года, возможно, вместе с тиками для лучшей точности кругового обхода, но дело в том, что переопределенный метод ReadJson JsonConverter никогда даже не получит возможности запустить, предположительно потому что DateTime - это тип значения, а не класс?   -  person Triynko    schedule 26.11.2013
comment
Можете показать код конвертера?   -  person Brian Rogers    schedule 26.11.2013
comment
Я мог бы, но это ничего не стоит. Он просто расширяет встроенный JsonConverter и переопределяет методы WriteJson и ReadJson. Метод WriteJson работает нормально и выдает отображаемую мной строку JSON. Проблема в том, что, несмотря на то, что строка включает "$type":"System.DateTime", метод конвертера CanConvert никогда не вызывается с этим типом, а метод ReadJson конвертера никогда не вызывается. Как будто он даже не пытается использовать мой конвертер и даже не тестирует, можно ли его использовать. Возможно, это проблема обязательности, но я думал, что это произошло автоматически.   -  person Triynko    schedule 26.11.2013
comment
Я также попытался изменить метод WriteJson, чтобы записать объект как пользовательский класс ссылочного типа с именем DateTimeWrapper, который просто хранит отдельные целочисленные отметки, поэтому строка {"$type":"mynamespace.DateTimeWrapper","ticks":0} создается сериализацией, но, как ни странно, при попытке десериализации строки Вызывается метод CanConvert моего JsonConverter, но он получает целочисленный тип вместо типа mynamespace.DateTimeWrapper. Я считаю это странным. Как и предполагалось, он игнорирует $ type, встроенный в строку объекта JSON во время десериализации.   -  person Triynko    schedule 26.11.2013
comment
Я просто смог воспроизвести проблему. Вы правы, конвертер не вызывается, если вы просто пытаетесь десериализовать пустой DateTime. Но если вы заключите DateTime в другой объект, это сработает.   -  person Brian Rogers    schedule 26.11.2013
comment
У этого парня такая же проблема: stackoverflow.com/questions/14887389/   -  person Triynko    schedule 26.11.2013
comment
Ну, он не работает и в другом объекте. Тип $ присутствует, но он даже не пытается использовать мой JsonConverter (т.е. он даже не вызывает CanConvert, чтобы узнать, подходит ли он для этого типа). Он просто пытается создать простой экземпляр типа самостоятельно.   -  person Triynko    schedule 26.11.2013
comment
Подождите - позвольте мне опубликовать код, который я использую, который работает. Вы можете попробовать это.   -  person Brian Rogers    schedule 26.11.2013
comment
Ой, подождите, если вы не говорите, что он неправильно обрабатывает тип, когда объект является объектом верхнего уровня в строке. В этом случае либо это ошибка, либо я неправильно использую класс JsonConvert.   -  person Triynko    schedule 26.11.2013
comment
Да, это то, о чем я говорю: если DateTime - это верхний уровень, это не работает, но когда это не так, то работает. Так что, если ваше свидание никогда не будет на высшем уровне, тогда все будет в порядке.   -  person Brian Rogers    schedule 26.11.2013
comment
Я думаю, что сейчас происходит то, что список преобразователей на самом деле не применяется к объектам, а скорее применяется только к членам объектов. Думаю, это какой-то серьезный недостаток дизайна.   -  person Triynko    schedule 26.11.2013
comment
Другими словами, объект верхнего уровня может быть строго типизирован, но не может быть обработан преобразователем. Он будет создан как строго типизированный объект, и его члены будут запускаться через преобразователи, но сам объект верхнего уровня - нет. Это означает, что объект верхнего уровня никогда не может быть типом, который должен обрабатываться преобразователем.   -  person Triynko    schedule 26.11.2013
comment
Я не уверен, что зашел бы так далеко - вы пробовали это с нестандартным типом? Возможно, проблема в DateTime.   -  person Brian Rogers    schedule 26.11.2013
comment
Да, пробовал с нестандартным типом. Та же проблема, что и у другого парня, и на самом деле то, что я только что сказал, не объясняет его проблемы. Его объекты SantaClause уже находятся в массиве, поэтому они не верхнего уровня, и он испытывает ту же проблему, что CanConvert даже не вызывается для каждого объекта.   -  person Triynko    schedule 26.11.2013


Ответы (2)


Проблема, похоже, связана с десериализацией голой даты. Когда Date заключен в другой объект, кажется, что это работает. У меня работает этот код:

public class Program
{
    public static void Main(string[] args)
    {
        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
        settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
        settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
        settings.ConstructorHandling = ConstructorHandling.Default;
        settings.TypeNameHandling = TypeNameHandling.All;
        settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
        settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
        settings.DateParseHandling = DateParseHandling.DateTime;
        settings.Converters.Add(new AS3DateTimeConverter());

        TestObject obj = new TestObject { Date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) };
        string s = JsonConvert.SerializeObject(obj, settings);
        Console.WriteLine(s);
        object o = JsonConvert.DeserializeObject(s, settings);
        Console.WriteLine(((TestObject)o).Date.ToString());
    }
}

public class TestObject
{
    public DateTime Date { get; set; }
}

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject jo = new JObject();
        jo.Add("$type", "System.DateTime, mscorlib");
        jo.Add("ticks", ((DateTime)value).Ticks);
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        return new DateTime(jo["ticks"].Value<long>());
    }
}

Вывод:

{"$id":"1","$type":"Q20224027.TestObject, JsonTest","Date":{"$type":"System.DateTime, mscorlib","ticks":621355968000000000}}
1/1/1970 12:00:00 AM

ОБНОВЛЕНИЕ

Чтобы проверить теорию о том, вызываются ли преобразователи для пользовательских объектов верхнего уровня со встроенной информацией о типе, я сделал преобразователь для объекта-оболочки даты и сериализовал его вместо этого. Это сработало, но только если я намекнул, используя DeserializeObject<T> вместо DeserializeObject. Вот код:

namespace Q20224027
{
    public class Program
    {
        public static void Main(string[] args)
        {
            JsonSerializerSettings settings = new JsonSerializerSettings();
            settings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
            settings.PreserveReferencesHandling = PreserveReferencesHandling.All;
            settings.ObjectCreationHandling = ObjectCreationHandling.Replace;
            settings.ConstructorHandling = ConstructorHandling.Default;
            settings.TypeNameHandling = TypeNameHandling.All;
            settings.TypeNameAssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
            settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
            settings.DateParseHandling = DateParseHandling.DateTime;
            settings.Converters.Add(new DateWrapperConverter());

            DateWrapper obj = new DateWrapper { Date = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) };
            string s = JsonConvert.SerializeObject(obj, settings);
            Console.WriteLine(s);
            object o = JsonConvert.DeserializeObject<DateWrapper>(s, settings);
            Console.WriteLine(((DateWrapper)o).Date.ToString());
        }
    }

    public class DateWrapper
    {
        public DateTime Date { get; set; }
    }

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

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            DateWrapper obj = (DateWrapper)value;
            JObject jo = new JObject();
            jo.Add("$type", typeof(DateWrapper).AssemblyQualifiedName);
            jo.Add("ticks", obj.Date.Ticks);
            jo.WriteTo(writer);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            return new DateWrapper { Date = new DateTime(jo["ticks"].Value<long>()) };
        }
    }
}

Вывод:

{"$type":"Q20224027.DateWrapper, JsonTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","ticks":621355968000000000}
1/1/1970 12:00:00 AM
person Brian Rogers    schedule 26.11.2013
comment
Это работает, но, опять же, у класса верхнего уровня никогда не будет вызова конвертера. Вы можете подтвердить поведение, добавив еще один преобразователь для типа TestObject и запустив тот же код. Он никогда не будет вызывать метод CanConvert объекта TestObjectConverter. - person Triynko; 26.11.2013
comment
Я начинаю задаваться вопросом, проблема ли это в разрешении сборки. Будет ли разница в том, что мой AS3DateTimeConverter находится в сборке, отличной от сборки основного приложения? Это статическая ссылка, поэтому это не должно быть проблемой, хотя я заметил, что она не может найти тип, если я не вставлю имя сборки в $ type. - person Triynko; 26.11.2013
comment
Если он находится в другой сборке, вам, вероятно, нужно встроить имя сборки в $type так же, как вы это сделали для DateTime (часть mscorlib). - person Brian Rogers; 26.11.2013
comment
Нет, проблема не в этом. Я использовал ваш код и снова выполнил тест со всем в одной сборке, и проблема все еще существует с объектами верхнего уровня, которые не подвергаются десериализации с назначенными им преобразователями, несмотря на сериализацию с ними. - person Triynko; 26.11.2013
comment
Я создал конвертер для оболочки TestObject и снова обнаружил, что, хотя метод конвертера WriteJson вызывается во время сериализации и тип правильно встроен, конвертер игнорируется при десериализации и не вызываются ни методы CanConvert, ни методы ReadJson. - person Triynko; 26.11.2013
comment
Хм, у меня работает. Что ты делаешь не так, как я? Я обновил свой ответ. - person Brian Rogers; 26.11.2013
comment
О, он работает нормально, но поставьте точку останова в свой метод ReadJson ... держите пари, что он не попадет. Он создает TestObject самостоятельно во время десериализации с помощью конструктора и присваивания по умолчанию, а не с помощью метода ReadJson преобразователя. - person Triynko; 26.11.2013
comment
О, черт, ты прав - я только что заметил, что дата не указана на выходе! Я отзываю свое заявление. - person Brian Rogers; 26.11.2013
comment
Вероятно, вы получите текущую дату, потому что экземпляр с голыми костями получает значение по умолчанию. Если бы в ваших настройках отключено игнорирование дополнительных участников или что-то в этом роде, вы также получили бы сообщение об ошибке. Итак, я собираюсь пойти дальше и сказать, что объекты верхнего уровня не могут использовать преобразователи, и это своего рода ошибка во время десериализации. - person Triynko; 26.11.2013
comment
Вы можете заставить его работать, если дадите ему подсказку, используя DeserializeObject<T> вместо DeserializeObject. - person Brian Rogers; 26.11.2013
comment
Да, я тоже это заметил. Но я не считал это подходящим обходным путем, потому что при получении строки JSON от клиента нельзя просто предполагать определенный тип (или, по крайней мере, не должно быть), поскольку тип $ должен быть встроен. Моя система предназначена для беспрепятственного взаимодействия между AS3 и .NET, и у меня даже есть привязка, настроенная в web.config для сопоставления типов AS3 с типами .NET. Все это отлично работает при использовании простых старых объектов, но когда некоторые несовместимые типы, такие как Date ›DateTime или ByteArray› System.Byte [], требуют специальной работы, тогда это становится проблемой для этих типов как объектов верхнего уровня. - person Triynko; 26.11.2013
comment
И в этом проблема ... необходимость жестко запрограммировать ожидаемый тип нарушает всю функцию встраивания $ type в строку для последующей десериализации. - person Triynko; 26.11.2013
comment
Понял. Возможно, вам стоит сообщить об этом Джеймсу Ньютон-Кингу и посмотреть, что он скажет по этому поводу. Или вы можете попробовать изменить исходный код самостоятельно и попытаться создать исправление, а затем отправить запрос на перенос. - person Brian Rogers; 26.11.2013
comment
Забавно, что вы сказали это, смех, см. codeplex.com/site/users/view/ JamesNK - Личное заявление JamesNK: если у вас есть вопрос о проекте, и вы не смогли найти ответ в какой-либо документации, опубликуйте вопрос в Stackoverflow. Я не буду отвечать на письма службы поддержки напрямую. Да, в том числе и вы. Во всяком случае, я опубликовал отчет об ошибке, который ссылается на этот пост. - person Triynko; 28.11.2013
comment
LOL, ну, тогда я думаю, ты сделал то, что мог! Удачи тебе. - person Brian Rogers; 28.11.2013

Я нашел обходной путь.

Если объект помещен в список перед сериализацией, он работает нормально, но только в том случае, если вы украсите класс атрибутом JsonConverter, указывающим преобразователь. Недостаточно добавить конвертер в список конвертеров для настроек сериализаторов.

Например, если у вас есть класс «Узел», который имеет член «Дочерний» Узел (т.е. тип имеет элементы своего собственного типа), и вы вкладываете некоторые узлы, то я обнаружил, что преобразователь не вызывается во время сериализации для чего-либо, кроме верхнего узла, когда вы только добавляете преобразователь в список преобразователей. С другой стороны, если вы явно украшаете класс конвертером, то все дочерние узлы запускаются через метод WriteJson конвертера, как и ожидалось. Таким образом, это в основном делает нефункциональным набор настроек сериализатора «Конвертеры».

Когда объекты являются членами массива и их тип украшен явным преобразователем, тогда методы их преобразователя ReadJson и WriteJson вызываются, когда типы встречаются во время сериализации и десериализации.

При получении строки JSON от клиента есть только два способа заставить ее работать. Либо вы вручную оборачиваете строку в объект, включающий общий список «$ type», и вставляете полученное значение как единственное значение в массив «$ values», либо вы должны просто избегать всего этого и жестко закодировать ожидаемый тип полученного объекта. вызывая типизированный DeserializeObject<T> метод. Какой беспорядок.

Единственная причина, по которой я могу понять, что это имеет смысл, заключается в том, что метод DeserializeObject (неуниверсальный) был явно предназначен НЕ для вызова преобразователей для объекта верхнего уровня, предположительно для того, чтобы его можно было использовать в методе WriteJson пользовательского преобразователя, не вызывая рекурсивного звонки на конвертер. Если это так, дизайн ужасен, потому что он приводит ко всем проблемам, которые я обсуждал.

person Triynko    schedule 02.12.2013