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-валідації: ${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» — ви втрачаєте головну вигоду. Перевірка за схемою має бути раннім етапом: «вхідні дані або проходять контракт, або ми далі не йдемо».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ