1. “XML reads” vs “XML is correct”
When you’re just starting to work with XML, it can feel like if the document “parses”, everything must be fine. But in reality, “parses” is roughly like “the car starts”: nice, but it doesn’t mean the wheels are bolted on. XSD is needed to formally describe what structure is considered acceptable and to validate incoming documents before you start extracting data and building logic on top of them.
Let’s immediately separate three different levels of “correctness”, because people confuse them more often than == and = in their first programs:
| Level | What it means | Who “complains” |
|---|---|---|
| Well-formed | XML is syntactically correct: tags are closed, nesting is correct | XML parser (DOM parser) |
| Valid (per XSD) | XML matches the schema: required elements/attributes are present, value types fit | XSD validator |
| Business-meaning correct | Values make sense for your program (for example, amount > 0, category is from an allowed set) | Your Kotlin code |
And here’s the key idea: XSD sits in the middle. It’s not about “brackets and quotes” (that’s well-formed), but it’s also not about “meaning being correct” (that’s your business validation).
2. What XSD is and what a schema looks like
What XSD is
XSD (XML Schema Definition) is a document that describes the structure contract of XML. Funny thing: XSD itself also looks like XML. Yes, this is the case where the “meta level” looks like “we wrote a document that describes documents, and it’s also a document”. Programmers do this all the time: a compiler compiles a compiler, tests test tests... it’s fine, we’re not the only ones.
In practice, XSD answers questions like: what the root element must be, which child elements are allowed and in what order, which attributes are required and which are optional, and what type a value has (string, int, decimal, etc.).
And this is where XSD is especially useful in integrations, when you have “incoming XML from another system”. You can’t forbid the other system from making mistakes. But you can learn to quickly and honestly say: “the document does not conform to the contract”.
Minimal schema example
Let’s imagine we’re continuing our training console project (let’s call it ExpenseBook)—an app where we accumulate expenses and build reports. Earlier we stored data in collections, then we learned to serialize to JSON, and now we want to support import from XML (because some accounting department loves XML, and convincing them otherwise is pointless).
Suppose the XML format is:
<expenses>
<expense id="1" amount="199" category="food">Lunch</expense>
<expense id="2" amount="350" category="transport">Taxi</expense>
</expenses>
And we want:
- the root to be expenses;
- inside there to be expense;
- each expense must have id (integer) and amount (integer);
- category is required and is a string;
- the text inside is a description (it may be empty, but the element exists).
An XSD schema (minimal, not an “enterprise monster”) can look like this:
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()
Yes, it looks verbose. This is XML. It’s like that. It has a union responsible for verbosity.
3. Validation on the JVM: SchemaFactory → Validator
Pipeline: Schema → Validator → validate(...)
Now the most practical part: how to validate this in Kotlin/JVM.
The overall flow looks like this:
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["You can parse DOM/XPath"]
H -->|Exception| J["Report: does not match XSD"]
The key point: in the minimal variant, validation throws an exception if the document does not conform to the schema. That’s normal: validation is “strict control”, and on mismatch it doesn’t return false—it literally “breaks execution”. And we already know how to live with that via try/catch.
By the way, this “didn’t work—throw an exception” style pairs well with the idea of fail-fast and preconditions (require/check) from earlier topics about exceptions.
Minimal implementation: validate from strings
Here we make a function “validate XML against XSD” that returns Boolean. This isn’t the only design, but it’s the most beginner-friendly: either true or false, without extra philosophy.
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
}
}
Notice two things. First, we use StringReader to turn a string into a “character stream” that StreamSource understands. Second, we catch Exception very broadly, because in an overview lecture it’s more important to “get the idea” than to perfectly categorize exceptions.
Demo: valid XML and “almost the same, but broken”
Now let’s write a small main that validates two documents: one correct and one with a type error (for example, id="oops" instead of a number).
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
}
This is the “minimally understandable” example: XSD requires id as xs:int, and the document with oops doesn’t pass the schema.
A bit better than Boolean: return an error message
When you actually use validation in an app, false is often not enough. The user (or you while debugging) will want to understand: “what exactly is wrong?”. So let’s create a small result model:
data class XsdValidationResult(
val isValid: Boolean,
val message: String?,
)
And a function that returns a result with a message:
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 matches XSD")
} catch (e: Exception) {
XsdValidationResult(isValid = false, message = "XSD validation failed: ${e.message}")
}
}
Here we do exactly the same thing, but now we have a chance to show a person the reason. The exception message may not be the prettiest (an XSD validator isn’t always a “friendly psychologist”), but for diagnostics it’s already a huge step.
4. How to integrate XSD into an XML pipeline
The main practical value of XSD is to move problems to the beginning of the pipeline. If the document doesn’t match the schema, we shouldn’t try to pull //expense[@id='...'] out of it, build reports, and then be surprised by empty values. We simply say: “The document doesn’t match the contract; we don’t go further.”
Let’s imagine that in previous lectures we already have a function parseExpensesFromXml(xml: String): List<Expense> that does DOM/XPath extraction. Then the “correct entry point” for import looks like this (simplified):
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, you can parse DOM/XPath") // sample output
}
Note the style: we do a “guard check” and an early return. This is the guard clauses approach you’ve already seen in topics about preconditions and checks, and it usually makes code easier to read.
Reading XML/XSD from files: same approach, just a different source
In the real world, XSD often sits in a separate file next to the application (or is delivered as part of the integration). XML also comes from a file. The overall idea doesn’t change: you read text and pass it into the same functions.
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 ?: "-") // message or "-"
}
It’s important to remember an old but useful habit: if something works with strings, then the source of those strings (file, network, stdin) can change, while the validation code stays the same.
5. The limits of XSD: what it validates and what it doesn’t
It’s very tempting (especially after your first successful validation) to start thinking: “Oh! Now XSD guarantees the data is correct.” And that’s the trap.
XSD is great for structure and types: whether an element exists, whether an attribute exists, whether it’s a number or a string, whether an element repeats the required number of times. But XSD usually doesn’t solve your business problems.
For example, XSD can say: “amount is an int”, but it doesn’t have to guarantee: “amount > 0”, unless you explicitly describe that (and in XSD it might not be as trivial as you’d like). It won’t check that id is unique among all <expense>. It won’t know that the category must be from a specific set unless you put that set into the schema. And it definitely won’t understand that you shouldn’t pay for “taxi” 37 times per minute... although sometimes you wish the validator would step into your life and block questionable decisions.
So a good mental model is: XSD is an input filter for coarse structural errors, and business validation is the next layer in your Kotlin code.
Why this tool is marked as optional
XSD is useful, but not universal. In practice, many systems use XML “by agreement” without a schema, and some even live in “something arrived—we’ll parse it with regexes” mode (yes, that happens too, and yes, it’s scary). Also, XSD can be fairly large and hard to maintain: the format changes—you need to change the schema and agree on versions.
So today our goal isn’t to learn how to write huge XSD documents. The goal is for you to recognize the “schema validation” mechanism and be able to do a minimal conformance check when a schema already exists (or when you want to fix a small contract for your import).
6. Common mistakes when using XSD validation
Mistake #1: confusing well-formed and valid.
Beginners often think: “If the parser didn’t crash, then XSD will pass too.” In reality these are different levels. XML can be perfectly well-formed (all tags closed) but invalid per the schema (for example, a required attribute is missing). And conversely: if the XML isn’t well-formed, you won’t even get to XSD—the parser stops you earlier.
Mistake #2: expecting validate(...) to return false instead of throwing an exception.
Validation in the Java API is built around exceptions: “does not match” means it’s a validation-process error, and it’s reported via an exception. That’s why try/catch around validate(...) is not decoration, but a normal working scenario.
Mistake #3: swallowing the exception and losing the cause.
If you do catch (e: Exception) { return false }, you deprive yourself of diagnostics. During development it’s better to return the message or at least print e.message, otherwise you’ll be fixing the format “blindly”, like hunting bugs with your monitor turned off.
Mistake #4: treating XSD as a replacement for business validation.
XSD validates structure perfectly, but it doesn’t have to validate meaning. Even if you set xs:int, XSD doesn’t guarantee the number is “reasonable” for your domain. So after XSD you usually still do checks in Kotlin: ranges, allowed values, uniqueness, field consistency. For such checks, it’s convenient to use a fail-fast approach and preconditions (require, check) where the program must not continue on invalid data.
Mistake #5: trying to write an XSD “quickly” and accidentally locking in the wrong contract.
Sometimes a schema is written “by eye” and then starts living as an official format document, even though it already contains hidden mistakes: wrong required/optional flags, wrong types, wrong nesting. After that you get a paradox: the XML is “correct in meaning”, but “incorrect per schema”, and the whole team argues about who’s at fault—the XML author or the XSD author. Discipline helps here: a schema is a contract, and it should be treated like an API.
Mistake #6: validating too late (after DOM/XPath).
If you parse the DOM first, extract pieces, build intermediate structures, and only then decide “by the way, let’s validate XSD”—you lose the main benefit. Schema validation should be an early step: “either the input data passes the contract, or we don’t go further.”
GO TO FULL VERSION