JavaRush /Курсы /Kotlin SELF /XSD: зачем нужна схема и проверка соответствия

XSD: зачем нужна схема и проверка соответствия

Kotlin SELF
50 уровень , 3 лекция
Открыта

1. “XML читается” vs “XML правильный”

Когда вы только начинаете работать с XML, кажется, что если документ “парсится”, значит всё хорошо. Но в реальности “парсится” — это примерно как “машина заводится”: приятно, но не означает, что колёса прикручены. XSD нужен, чтобы формально описать, какая структура считается допустимой, и проверить входящие документы до того, как вы начнёте извлекать из них данные и строить логику.

Давайте сразу разведём три разных уровня “корректности”, потому что их путают чаще, чем == и = в первых программах:

Уровень Что это значит Кто “ругается”
Well-formed XML синтаксически корректен: теги закрыты, вложенность правильная XML-парсер (DOM-парсер)
Valid (по XSD) XML соответствует схеме: нужные элементы/атрибуты есть, типы значений подходят XSD-валидатор
Корректен по бизнес-смыслу Значения логичны для вашей программы (например, amount > 0, категория из разрешённого набора) Ваш Kotlin-код

И вот здесь важная мысль: XSD сидит посередине. Он не про “скобочки и кавычки” (это well-formed), но и не про “правильный смысл” (это уже ваша бизнес-валидация).

2. Что такое XSD и как выглядит схема

Что такое XSD

XSD (XML Schema Definition) — это документ, который описывает контракт структуры XML. Забавно, но XSD сам по себе тоже выглядит как XML. Да, это тот случай, когда “мета-уровень” выглядит как “мы написали документ, который описывает документы, и он тоже документ”. Программисты так делают постоянно: компилятор компилирует компилятор, тесты тестируют тесты… всё нормально, мы не одни такие.

На практике XSD отвечает на вопросы вроде: какой корневой элемент должен быть, какие дочерние элементы допустимы и в каком порядке, какие атрибуты обязательны, а какие нет, какого типа значение (string, int, decimal, и т.д.).

И вот тут XSD особенно полезен в интеграциях, где у вас есть “входящий XML от другой системы”. Вы не можете запретить другой системе ошибаться. Но вы можете научиться быстро и честно говорить: “документ не соответствует контракту”.

Минимальный пример схемы

Представим, что мы продолжаем наш учебный консольный проект (условно назовём его ExpenseBook) — приложение, где мы копим расходы и строим отчёты. Раньше мы хранили данные в коллекциях, потом научились сериализовать в JSON, а теперь хотим поддержать импорт из XML (потому что какая-нибудь бухгалтерия любит XML, и переубеждать её бесполезно).

Пусть формат XML такой:

<expenses>
  <expense id="1" amount="199" category="food">Lunch</expense>
  <expense id="2" amount="350" category="transport">Taxi</expense>
</expenses>

И мы хотим, чтобы:

  • корень был expenses;
  • внутри были expense;
  • у каждого expense обязателен id (целое) и amount (целое);
  • category обязателен и строка;
  • текст внутри — описание (может быть пустым, но элемент есть).

Схема XSD (минимальная, не “энтерпрайз-монстр”) может выглядеть так:

val expensesXsd = """
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
      <xs:element name="expenses">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="expense" maxOccurs="unbounded">
              <xs:complexType>
                <xs:simpleContent>
                  <xs:extension base="xs:string">
                    <xs:attribute name="id" type="xs:int" use="required"/>
                    <xs:attribute name="amount" type="xs:int" use="required"/>
                    <xs:attribute name="category" type="xs:string" use="required"/>
                  </xs:extension>
                </xs:simpleContent>
              </xs:complexType>
            </xs:element>
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:schema>
""".trimIndent()

Да, выглядит многословно. Это XML. Он такой. У него за многословие отвечает профсоюз.

3. Валидация на JVM: SchemaFactoryValidator

Pipeline: Schema → Validator → validate(...)

Теперь самое практичное: как это проверять в Kotlin/JVM.

Общий поток выглядит так:

flowchart TD
    A["XML (String/File)"] --> B["StreamSource"]
    C["XSD (String/File)"] --> D["StreamSource"]
    D --> E["SchemaFactory.newSchema(...)"]
    E --> F["Schema"]
    F --> G["Validator"]
    B --> H["validate(xmlSource)"]
    G --> H
    H -->|OK| I["Можно парсить DOM/XPath"]
    H -->|Exception| J["Сообщаем: не соответствует XSD"]

Ключевой момент: в минимальном варианте валидация бросает исключение, если документ не соответствует схеме. Это нормально: валидация — “жёсткий контроль”, и при несоответствии она не возвращает false, а именно “ломает выполнение”. А мы уже умеем с этим жить через try/catch.

Кстати, именно такой стиль “не получилось — бросили исключение” хорошо сочетается с идеей fail-fast и предусловий (require/check) из прошлых тем про исключения.

Минимальная реализация проверки из строки

Здесь мы делаем функцию “проверить XML по XSD”, которая возвращает Boolean. Это не единственный дизайн, но он самый дружелюбный для новичка: либо true, либо false, без лишней философии.

import java.io.StringReader
import javax.xml.XMLConstants
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.SchemaFactory

fun validateXmlAgainstXsd(xml: String, xsd: String): Boolean {
    return try {
        val schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
            .newSchema(StreamSource(StringReader(xsd)))

        schema.newValidator().validate(StreamSource(StringReader(xml)))
        true
    } catch (e: Exception) {
        false
    }
}

Обратите внимание на две вещи. Во‑первых, мы используем StringReader, чтобы превратить строку в “поток символов”, который понимают StreamSource. Во‑вторых, мы ловим Exception максимально широко, потому что в обзорной лекции нам важнее “понять идею”, чем идеально разложить исключения по полочкам.

Демонстрация: валидный XML и “почти такой же, но сломанный”

Сейчас сделаем небольшой main, который проверяет два документа: один правильный, другой — с ошибкой типа (например, id="oops" вместо числа).

fun main() {
    val xsd = """
        <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
          <xs:element name="user">
            <xs:complexType>
              <xs:sequence>
                <xs:element name="name" type="xs:string"/>
              </xs:sequence>
              <xs:attribute name="id" type="xs:int" use="required"/>
            </xs:complexType>
          </xs:element>
        </xs:schema>
    """.trimIndent()

    val okXml = """<user id="42"><name>Ann</name></user>"""
    val badXml = """<user id="oops"><name>Ann</name></user>"""

    println(validateXmlAgainstXsd(okXml, xsd))  // true
    println(validateXmlAgainstXsd(badXml, xsd)) // false
}

Это тот самый “минимально понятный” пример: XSD требует id как xs:int, и документ с oops схему не проходит.

Чуть лучше, чем Boolean: возвращаем сообщение об ошибке

Когда вы реально используете валидацию в приложении, false часто недостаточно. Пользователь (или вы в отладке) захочет понять: “а что именно не так?”. Поэтому сделаем маленькую модель результата:

data class XsdValidationResult(
    val isValid: Boolean,
    val message: String?,
)

И функцию, которая возвращает результат с сообщением:

import java.io.StringReader
import javax.xml.XMLConstants
import javax.xml.transform.stream.StreamSource
import javax.xml.validation.SchemaFactory

fun validateXmlAgainstXsdDetailed(xml: String, xsd: String): XsdValidationResult {
    return try {
        val schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
            .newSchema(StreamSource(StringReader(xsd)))

        schema.newValidator().validate(StreamSource(StringReader(xml)))
        XsdValidationResult(isValid = true, message = "OK: XML соответствует XSD")
    } catch (e: Exception) {
        XsdValidationResult(isValid = false, message = "XSD validation failed: ${e.message}")
    }
}

Тут мы делаем ровно то же самое, но теперь у нас есть шанс показать человеку причину. Сообщение исключения может быть не самым красивым (XSD-валидатор не всегда “дружелюбный психолог”), но для диагностики это уже огромный шаг.

4. Как встроить XSD в XML-пайплайн

Главная практическая ценность XSD — сдвинуть проблемы в начало пайплайна. Если документ не соответствует схеме, мы не должны пытаться доставать из него //expense[@id='...'], строить отчёты и потом удивляться пустым значениям. Мы просто говорим: “Документ не соответствует контракту, дальше не идём”.

Представим, что в прошлых лекциях у нас уже есть функция parseExpensesFromXml(xml: String): List<Expense>, которая делает DOM/XPath извлечение. Тогда “правильный вход” в импорт будет выглядеть так (упрощённо):

fun importExpensesXml(xml: String, xsd: String) {
    val validation = validateXmlAgainstXsdDetailed(xml, xsd)
    if (!validation.isValid) {
        println(validation.message) // XSD validation failed: ...
        return
    }

    println("XML structure is OK, можно парсить DOM/XPath") // примерный вывод
}

Обратите внимание на стиль: мы делаем “охранную проверку” и ранний выход. Это тот самый подход guard clauses, который вы уже видели в темах про предусловия и проверки, и он обычно делает код проще для чтения.

Чтение XML/XSD из файлов: тот же подход, просто другой источник

В реальном мире XSD часто лежит отдельным файлом рядом с приложением (или поставляется вместе с интеграцией). XML тоже приходит из файла. Тогда общий смысл не меняется: вы читаете текст и передаёте в те же функции.

import java.io.File

fun main() {
    val xml = File("expenses.xml").readText()
    val xsd = File("expenses.xsd").readText()

    val result = validateXmlAgainstXsdDetailed(xml, xsd)
    println(result.isValid)          // true/false
    println(result.message ?: "-")   // сообщение или "-"
}

Тут важно помнить старую, но полезную привычку: если что-то работает со строками, то источник строк (файл, сеть, stdin) можно менять, а код валидации остаётся прежним.

5. Границы XSD: что он проверяет, а что нет

Очень хочется (особенно после первой удачной валидации) начать думать: “О! Теперь XSD гарантирует, что данные правильные”. И вот тут ловушка.

XSD отлично подходит для структуры и типов: есть ли элемент, есть ли атрибут, число ли это, строка ли это, повторяется ли элемент сколько надо. Но XSD обычно не решает ваши бизнес-задачи.

Например, XSD может сказать: “amount — это int”, но не обязан гарантировать: “amount > 0”, если вы это явно не описали (а в XSD это может быть не так тривиально, как хочется). Он не проверит, что id уникальный среди всех <expense>. Он не узнает, что категория должна быть из конкретного набора, если вы не внесли этот набор в схему. И уж точно он не поймёт, что “taxi” нельзя оплачивать 37 раз в минуту… хотя иногда хотелось бы, чтобы валидатор вмешивался в нашу жизнь и запрещал сомнительные решения.

Поэтому хорошая “ментальная модель” такая: XSD — это входной фильтр от грубых структурных ошибок, а бизнес-валидация — это следующий слой вашего Kotlin-кода.

Почему этот инструмент помечен как optional

XSD — инструмент полезный, но не универсальный. На практике многие системы используют XML “по договорённости” без схемы, а некоторые вообще живут в режиме “пришло что-то — разберём регулярками” (да, так тоже бывает, и да, это страшно). Кроме того, XSD может быть довольно объёмным и сложным в поддержке: поменялся формат — нужно менять схему и договариваться о версиях.

Поэтому сегодня наша цель не в том, чтобы научиться писать огромные XSD-документы. Цель в том, чтобы вы узнавали механизм “проверка по схеме” и умели сделать минимальную проверку соответствия, когда схема у вас уже есть (или когда вы хотите зафиксировать маленький контракт для своего импорта).

6. Типичные ошибки при использовании XSD-валидации

Ошибка №1: путать well-formed и valid.
Часто новички думают: “Раз парсер не упал — значит XSD тоже пройдёт”. На самом деле это разные уровни. XML может быть идеально well-formed (все теги закрыты), но невалиден по схеме (например, не хватает обязательного атрибута). И наоборот: если XML не well-formed, до XSD вы даже не дойдёте — парсер остановит вас раньше.

Ошибка №2: ожидать, что validate(...) вернёт false, а не бросит исключение.
Валидация в Java API устроена через исключения: “не соответствует” — значит это ошибка процесса валидации, и она сигналится исключением. Поэтому try/catch вокруг validate(...) — не украшение, а нормальный рабочий сценарий.

Ошибка №3: проглатывать исключение и терять причину.
Если вы делаете catch (e: Exception) { return false }, вы лишаете себя диагностики. На этапе разработки лучше возвращать сообщение или хотя бы печатать e.message, иначе вы будете чинить формат “вслепую”, как будто ищете баги с выключенным монитором.

Ошибка №4: считать XSD заменой бизнес-валидации.
XSD прекрасно проверяет структуру, но не обязан проверять смысл. Даже если вы сделали xs:int, XSD не гарантирует, что число “разумное” для вашей предметной области. Поэтому после XSD обычно всё равно идут проверки в Kotlin: диапазоны, допустимые значения, уникальность, согласованность полей. Для таких проверок удобно использовать fail-fast подход и предусловия (require, check) там, где программа не должна продолжать работу при некорректных данных.

Ошибка №5: пытаться “быстро” написать XSD и случайно закрепить неправильный контракт.
Иногда схема пишется “на глазок” и начинает жить как официальный документ формата, хотя в ней уже спрятаны ошибки: неправильные обязательности, не те типы, неверная вложенность. После этого вы получаете парадокс: XML “по смыслу правильный”, но “по схеме неправильный”, и вся команда спорит, кто виноват — автор XML или автор XSD. Тут помогает дисциплина: схема — это контракт, и к ней стоит относиться как к API.

Ошибка №6: валидировать слишком поздно (после DOM/XPath).
Если вы сначала распарсили DOM, достали куски, наделали промежуточных структур, а потом решили “кстати, а давайте проверим XSD” — вы теряете главную выгоду. Проверка по схеме должна быть ранним этапом: “входные данные либо проходят контракт, либо мы дальше не идём”.

1
Задача
Kotlin SELF, 50 уровень, 3 лекция
Недоступна
Проверка пропуска
Проверка пропуска
1
Задача
Kotlin SELF, 50 уровень, 3 лекция
Недоступна
Отчёт валидатора
Отчёт валидатора
1
Задача
Kotlin SELF, 50 уровень, 3 лекция
Недоступна
Валидация по путям
Валидация по путям
1
Задача
Kotlin SELF, 50 уровень, 3 лекция
Недоступна
Сначала схема
Сначала схема
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ