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-валідації: ${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-валідації: ...
return
}
println("Структура XML 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» — ви втрачаєте головну вигоду. Перевірка за схемою має бути раннім етапом: «вхідні дані або проходять контракт, або ми далі не йдемо».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ