1. Why do we need XML if we have JSON?
XML is the thing many people first see as “scary angle brackets,” and then suddenly run into in real integrations: bank exports, enterprise buses, configs, old (and not so old) protocols. Our goal today is to learn to read XML with a developer’s eyes: understand what is data, what is structure, and why “but it looks nicely written” sometimes turns into a parser error.
If you’re thinking right now “JSON is simpler, why these angle brackets from 2007,” that’s a perfectly normal way to think. But reality likes to bring us not only fresh technologies, but also the ones where “everything is already set up, it’s been working for 15 years, don’t touch it.” XML is like that server in the storage closet: it may look scary, but it does important work and sometimes doesn’t even make noise.
XML is often chosen where you need strict structure, compatibility with legacy systems, or where there’s historically a lot of tooling around it: schemas, transformations, complex queries. At the same time, XML remains plain text — you can store it in a file, send it over the network, log it, compare it (carefully), and yes, accidentally break it with one extra character.
It’s important to separate two questions right away. First: “Can XML be parsed?” — that’s about format correctness (well‑formed). Second: “Does XML contain the right data for our task?” — that’s about business validation. Today we’re dealing with the first one: learning to read XML as a format and understand why it may fail to parse at all.
2. Basic XML structure
Element and tree
When you look at XML, it helps to switch your brain into “I see a tree” mode. Not a line, not text, not a set of characters, but a hierarchy: we have a root, the root has children, the children have their own children, and so on. In that sense, XML is very similar to JSON: it’s also a tree. It’s just written using tags in XML.
The smallest “building block” of XML is an element. An element has a name (the tag name), it may have attributes, and it has content (text and/or nested elements). The element itself is written as a pair of tags: an opening and a closing one.
Here is the most minimal XML example (one root element with text inside):
fun main() {
val xml = "<message>Hello</message>"
println(xml) // <message>Hello</message>
}
Visually it looks like a “wrapper around text.” But if you think in terms of a tree, it is:
- root: message
- text inside: Hello
For multi-line XML, it’s convenient to use """...""" and then trimIndent() so that indentation looks nice both in code and in the result:
fun main() {
val xml = """
<message>
Hello
</message>
""".trimIndent()
println(xml)
// <message>
// Hello
// </message>
}
Note: line breaks and spaces become part of the text. We’ll discuss later in detail why that matters (and why you sometimes suddenly have to write trim()).
Nesting and a single root
When XML gets a bit more complex, nesting appears. That’s literally “one element inside another.” And this is where rules start to resemble properly placed parentheses in math: open — close, and close in the correct order. A computer is very pedantic: it won’t accept “almost correct.” It doesn’t have a mode like “I get what you meant.”
Example: a user with id and a name inside:
fun main() {
val xml = """
<user id="42">
<name>Ann</name>
</user>
""".trimIndent()
println(xml)
}
If you represent this as a tree, you get:
flowchart TD
U["user (id=42)"]
N["name"]
T["text: Ann"]
U --> N --> T
Here user is the root element (root). And name is a nested element (child).
XML has a strict rule: the document must have exactly one root element. Not two “siblings,” not “but I have two blocks of data.” If you want two blocks — you must wrap them in a single common root.
Here’s an example that looks “logical,” but is considered an invalid XML document:
fun main() {
val broken = """
<a>1</a>
<b>2</b>
""".trimIndent()
println(broken)
// This is NOT a single XML document: two root elements
}
And this is already fine (there is one root root):
fun main() {
val ok = """
<root>
<a>1</a>
<b>2</b>
</root>
""".trimIndent()
println(ok)
}
3. Attributes and empty elements
When you start working with XML, attributes are the first thing that seems “obvious,” and then they mildly troll beginners. An attribute is a key="value" pair inside an opening tag. In the example above id="42" is an attribute of the user element.
The usual meaning of attributes is: “small properties of an element.” For example, an identifier, a code, a flag, a type. But it’s important to remember: an attribute value in XML is a string. Even if it says 42, the parser sees it as "42". In Kotlin, you will decide later: keep it as a string or convert it to a number (toIntOrNull()), like we did earlier.
Example with two attributes:
fun main() {
val xml = """<file name="report.xml" size="128" />"""
println(xml) // <file name="report.xml" size="128" />
}
Here we encounter another form of an element: an empty element (self‑closing tag) — <file ... />. This is simply a shortened notation for when there is nothing inside the element. That is, the following two notations are equivalent in meaning:
fun main() {
val a = "<file name=\\"report.xml\\" />"
val b = "<file name=\\"report.xml\\"></file>"
println(a) // <file name="report.xml" />
println(b) // <file name="report.xml"></file>
}
Note the escaping of quotes \" in a Kotlin string. In real code, you will more often use triple quotes to avoid escaping:
fun main() {
val xml = """<file name="report.xml" />"""
println(xml) // <file name="report.xml" />
}
4. Text and whitespace as data
When we format XML with nice indentation, it feels like spaces are “just for humans.” But the parser sees it differently: spaces and line breaks are text nodes (we’ll talk about nodes in detail in the next lecture, but it’s important to feel the idea already). So if you wrote:
<name>
Ann
</name>
then inside name there is text that looks roughly like "\n Ann\n" (line break, spaces, Ann, line break). In reality it depends a bit on the specific parser, but the general idea is: formatting becomes part of the text.
In practice, this means a simple thing: when you extract text from XML, you almost always do .trim(). So the habit “normalize the string after reading” (which we practiced with input and JSON) shows up all the time in XML.
A mini example in Kotlin to see “invisible” characters: we just print the string and its length.
fun main() {
val text = """
Ann
""".trimIndent()
println(text) // Ann
println(text.length) // 3
}
And now a similar idea, but with “internal” line breaks:
fun main() {
val xml = """
<name>
Ann
</name>
""".trimIndent()
println(xml)
println("xml length = ${'$'}{xml.length}") // xml length = ...
}
We are not parsing XML yet, but we already understand: formatting affects bytes/characters. This is not “philosophy”; it is the reason for half of the weird bugs like “why does the name have a leading space?”
5. well‑formed XML rules
The term well‑formed (literally “properly formed”) means: the XML follows the basic grammar and can be parsed as an XML document. This is not “the data is correct,” but “this is actually XML, not a set of characters pretending to be XML.”
You can think of well‑formed rules as rules for parentheses and quotes: if you violate them — that’s it, the parser stops. For practice, it’s convenient to keep this as a small table.
| Rule | What it means in plain language | Error example |
|---|---|---|
| Single root element | You cannot have two “top-level” elements side by side | |
| Every opened tag must be closed | If you opened <x>, close it with </x> | |
| Nesting must be correct | Close in reverse order (like parentheses) | |
| Tag names are case-sensitive | <Name> and </name> are different names | |
| Attributes must be in quotes | You can’t do id=42, you need id="42" | |
| Special characters in text must be escaped | & and < cannot be written “as-is” | |
Let’s look at a couple of typical “broken” examples so your brain starts recognizing them at first glance.
Incorrect nesting:
fun main() {
val broken = "<a><b></a></b>"
println(broken) // <a><b></a></b>
// The parser will say: "I don't understand what you're closing"
}
Unclosed tag:
fun main() {
val broken = "<user><name>Ann</user>"
println(broken) // <user><name>Ann</user>
// <name> is not closed, the document is invalid
}
Attribute without quotes:
fun main() {
val broken = "<user id=42></user>"
println(broken) // <user id=42></user>
// For XML this is an error: id must be in quotes
}
6. Escaping and entities
Now comes the part that breaks beginners most often. In XML, there are several characters that have special meaning. The most important ones are & and <. If you put them in text “as-is,” the XML can become unparseable, because the parser will think you are starting an entity (&...;) or a new tag (<tag>).
That’s why XML uses entities — special sequences that mean “insert the character, but safely.” The basic entities are:
| Character | How to write in XML | Where it appears |
|---|---|---|
|
|
in text, in attributes |
|
|
in text, in attributes |
|
|
usually allowed in text, but often escaped for symmetry |
|
|
especially important inside attr="..." |
|
|
when attributes are written in single quotes |
Example of “bad” XML:
fun main() {
val unsafe = "<title>Tom & Jerry</title>"
println(unsafe) // <title>Tom & Jerry</title>
// In XML this is NOT ok: & must be escaped
}
Correct version:
fun main() {
val safe = "<title>Tom & Jerry</title>"
println(safe) // <title>Tom & Jerry</title>
}
Mini utility: escaping text for XML
Writing XML by hand is an activity where it’s easy to commit a “symbolic crime” (accidentally insert &). So even in a learning app it’s useful to have a small function that escapes text. It doesn’t have to be perfect for every case in the world, but it must be understandable.
It’s important to preserve the order here: replace & first, and only then < and the rest. Otherwise you risk “double-escaping” the already inserted &.
fun escapeXmlText(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
}
This idea fits well with our “normalize and validate input data” approach. And yes, string templates in Kotlin make building strings much nicer, so next we’ll build XML using templates and run the text through escapeXmlText.
7. Practical example: exporting a report to an XML string
We continue developing our console data analyzer application (in the previous lecture it accepted JSON and built reports). Today we are not parsing XML, but we’ll add the ability to generate an XML export, because it’s a great way to reinforce: elements, attributes, text, escaping, and well‑formed.
Let’s imagine that inside the application we have a report result:
- report name
- number of records
- a short note (which may contain special characters)
Let’s write a function that creates XML:
fun buildReportXml(title: String, count: Int, note: String): String {
val safeTitle = escapeXmlText(title)
val safeNote = escapeXmlText(note)
return """
<report title="$safeTitle" count="$count">
<note>$safeNote</note>
</report>
""".trimIndent()
}
There are several “good” habits here at once. We use a multi-line string and trimIndent() so the XML is readable both in code and in the output. We explicitly escape the text so we don’t break the document with an & character. And we remember that count in an attribute will still become a string.
Now an example usage in main:
fun main() {
val xml = buildReportXml(
title = "Sales & Marketing",
count = 3,
note = "Top < 10 entries, ok"
)
println(xml)
// <report title="Sales & Marketing" count="3">
// <note>Top < 10 entries, ok</note>
// </report>
}
We got well‑formed XML that won’t fall apart because of & and <. And even if you don’t know how to parse XML yet, you already know how to form a valid document and can visually verify what’s happening there.
8. Common mistakes when working with XML as a format
Error #1: confusing “looks nice” with “well‑formed.”
A beginner looks at XML, sees indentation, line breaks, neat tags — and thinks that if it looks nice, it must be valid. But XML is not about beauty; it’s about strict grammar: one incorrectly closed tag or two roots in a row — and the parser will refuse to work. “Almost correct” for XML means “incorrect.”
Error #2: inserting text into XML without escaping.
This is a classic: you substitute a string containing & or < into <title>...</title>, and the document suddenly stops being XML. The most annoying part is that visually it looks “normal.” That’s why even in learning examples it’s useful to discipline yourself: run any user-provided text through escapeXmlText, otherwise you’ll catch errors in the most unexpected places.
Error #3: expecting spaces and line breaks “not to count.”
In XML, formatting is also text. If you write <name>\n Ann\n</name>, inside there will be text with spaces and line breaks. Then you wonder why string comparison doesn’t match or why extra spaces appear in output. In real XML processing there is almost always a text normalization step: at least trim().
Error #4: thinking attributes are “almost numbers/booleans.”
Attributes in XML are always strings. Even if you wrote count="3", it’s the string "3". So any “typing” (to Int, Boolean, enum) happens on your side. In that sense XML is similar to input via readln(): first a string, then an attempt to parse, then error handling.
Error #5: breaking the document with incorrect nesting.
<a><b></a></b> is an example that often appears when manually assembling strings or doing sloppy copy-paste. Think of tags as parentheses: close strictly in reverse order. If you catch yourself thinking “it’s hard to track with your eyes,” it’s time either to simplify the structure or to stop building XML by hand and switch to a proper parser/generator (but that’s the next step of the course).