Исключение при XML Deserialize

Как бросить исключение при десериализации если свойство в xml не будет найдено?

Например у меня был класс Persons и я его успешно сериализовал:

[Serializable]
[XmlRoot("Persons")]
public class Persons
{
    [XmlArray(nameof(Names)), XmlArrayItem(typeof(string), ElementName = "Name")]
    public List<string> Names { get; set; }
}

xml:

<?xml version="1.0"?>
<Persons xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Names>
    <Name>Ivan</Name>
    <Name>Alena</Name>
  </Names>
</Persons>

Затем мне понадобилось Names в классе Persons изменить на FullNames:

[Serializable]
[XmlRoot("Persons")]
public class Persons
{
    [XmlArray(nameof(FullNames)), XmlArrayItem(typeof(string), ElementName = "Name")]
    public List<string> FullNames { get; set; }
}

Как можно получить исключение при десериализации в случае когда FullNames не будет найдено в xml (предварительно xml не менялся, в нем все тот же старый Names):

try
{    
    using var fs = new FileStream(path, FileMode.Open);
    person = (Persons)serializer.Deserialize(fs);
}
catch (Exception ex)
{
    // Обработка исключения
}

Нужно чтобы обновить xml, создать новую по умолчанию с новым именем свойства. Сейчас я просто получаю пустой список FullNames.


Ответы (3 шт):

Автор решения: CrazyElf

Для XML ответ не актуален, только для JSON.
Но вдруг кому пригодится.

В .NET 7 можно использовать атрибут [JsonRequired]:

[JsonRequired]
public List<string> FullNames { get; set; }

Либо модификатор required из C# 11:

public required List<string> FullNames { get; set; }

Документация.

→ Ссылка
Автор решения: Alexander Petrov

Автор ввёл всех (во всяком случае, меня) в заблуждение фразой

Как бросить исключение при десериализации если свойство в xml не будет найдено?

и заключив код десериализации в блок try-catch.

Что означает исключение? Что произошло нечто из ряда вон выходящее и нужно откзаться от уже считанных данных. И в блоке catch заполнить класс дефолтными значениями.
В такой трактовке проблема решается следующим образом.

У сериализатора есть события. Можно на них подписаться и в событии выбросить исключение.

var serializer = new XmlSerializer(typeof(Persons));
serializer.UnknownElement += Serializer_UnknownElement;
Persons person;
try
{
    using var fs = new FileStream(path, FileMode.Open);
    person = (Persons)serializer.Deserialize(fs);
}
catch (Exception ex)
{
    person = new Persons { FullNames = new List<string> { "A", "B" } };
}

void Serializer_UnknownElement(object? sender, XmlElementEventArgs e)
{
    throw new NotImplementedException();
}

Здесь в обработчике исключения класс Persons создаётся со значениями по умолчанию.


Но, насколько я понял, нужно не прерывать процесс десериализации, а прочитать данные из XML со старыми названиями.
Это можно сделать множеством способов.

  1. Десериализуем XML в класс PersonsDTO, который имеет свойство Names. После чего мапим DTO на нужный объект Persons. Собственно, это стандартный современный способ с использованием промежуточного типа для передачи данных из слоя в слой.

  2. Опять используем событие и в нём вручную парсим вложенные элементы XML:

void Serializer_UnknownElement(object? sender, XmlElementEventArgs e)
{
    if (e.Element.Name == "Names")
    {
        var persons = (Persons)e.ObjectBeingDeserialized;
        var values = e.Element.ChildNodes.OfType<XmlElement>().Select(x => x.InnerText);
        persons.FullNames.AddRange(values);
    }
}

В данном конкретном случае это просто, потому что нам нужно разобрать всего один уровень вложенности с простым типом.
Но в общем случае это может быть трудоёмко. Мы же для того и взяли десериализатор, чтобы избежать ручного парсинга?

  1. Кастомный XmlReader. Мой любимый способ.
    Пишем класс-читатель, который на лету подменяет одно название элемента на другое. В нашем случае Names меняется на FullNames.
class NamesXmlReader : XmlTextReader
{
    public NamesXmlReader(string url) : base(url) { }

    public override string LocalName
    {
        get
        {
            if (base.LocalName == "Names")
                return "FullNames";
            return base.LocalName;
        }
    }
}

Далее, соответственно, используем его:

using var reader = new NamesXmlReader(path);
person = (Persons)serializer.Deserialize(reader);

Однако, нужно быть осторожным, т. к. в данном случае все элементы Names будут переименованы. А в сложном XML вполне могут быть совпадающие имена на разных уровня вложенности и в разных неймспейсах. В таком случае нужно вводить дополнительные проверки.

  1. Можно реализовать интерфейс IXmlSerializable у нашего типа Persons. Но это получается, по сути, ручной парсинг.

  2. Также можно с помощью LINQ2XML прочитать XML, что тоже является ручным разбором и отказом от автоматической десериализации. Хотя во многих случаях это реально проще, чем добиться от XmlSerializer'а желаемого поведения.

→ Ссылка
Автор решения: rotabor

Чтобы разобраться с этим вопросом, нужно понимать общую логику. Во-первых, XML это не совсем то же самое, что JSON. Во-вторых, парсер XML справедливо считает, что нужный класс просто не найден (есть другой класс).

Поэтому, чтение данных не поможет. Нужно извлечь из XML схему данных и проверить, есть ли в ней элемент "FullNames". Возможно, это делается так, пример.

→ Ссылка