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: SchemaFactory → Validator
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” — вы теряете главную выгоду. Проверка по схеме должна быть ранним этапом: “входные данные либо проходят контракт, либо мы дальше не идём”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ