JSP Documents (XML Syntax)

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Pages 4.0 (Jakarta EE 11), with Jakarta Expression Language 6.0, Jakarta Standard Tag Library 3.0 and Jakarta Servlet 6.1, running on Apache Tomcat 11.0.26 and Java 21. Everything was verified on 2026-10-08: the facts against the Pages 4.0 specification (chapters 5, 6, 8 and 10, section 3.3.7 and appendix B) and its Javadoc, and every example by building it into a Maven WAR and requesting it on Tomcat 11.0.26 with curl; the outputs and error messages shown are the ones that run produced. A JSP document is a JSP page that is also a well-formed, namespace-aware XML document. The container reads it as XML first and as a JSP page second, so the <% %> syntax is gone and every JSP construct has an XML form. (The container side, request dispatch and web.xml, is on Servlet Fundamentals.) The Bookshelf of this page serves its catalogue as XHTML, as plain XML, as a CSV text file and as a tag-file-based list, then uses the XML view (the representation that validators receive) to forbid scripting in a whole application.

What a JSP Document Is and Why It Exists

A JSP page in the usual syntax is a text file with some <% %> and ${ } islands. Most of what authors write is HTML, and HTML tolerates unclosed <br>, unquoted attributes and bare ampersands, so the usual syntax makes no attempt to be XML. That is fine for HTML, and a problem when the output must be XML: XHTML served as application/xhtml+xml, SVG, Atom or RSS feeds, a SOAP-era web-service reply, or a document that goes on to an XSLT pipeline. A JSP document keeps the page well-formed by construction, because a parser checks the file before any Java is generated.

The specification lists the intended uses: pass a document straight to the container, manipulate it with XML-aware tools, generate it from another XML document with a transformation, or generate it programmatically. It also separates two audiences. JSP documents are for page authors. The XML view is internal to the container and is of interest only to tag library authors and container implementors, because that is what a TagLibraryValidator sees (see The XML View).

Table 1. Status of the XML syntax in Jakarta Pages 4.0
Aspect Status (2026-10-08)

Specification

Current and normative: chapter 6 (JSP documents) and chapter 10 (XML view). Nothing in them was removed in Pages 4.0; the changes in appendix C are isThreadSafe, jsp:plugin, BodyTag.EVAL_BODY_TAG and ErrorData

Usage

Niche. Most applications generate HTML5, which is not well-formed XML, and use the usual syntax. The XML syntax is the better choice when the output must be XML

Validation

Only DTD validation is required for JSP documents. XML Schema validation and XInclude are listed as possible future directions (section 6.5), not as features

Engines

Tomcat (Jasper) 11.0.26 implements it, and every example below ran there. Only Tomcat was exercised for this page

Struts

The Struts part of this section (see Struts Getting Started) writes its views in the usual syntax; no Struts tag was exercised in a JSP document

Identifying a JSP Document

The container must decide, for every file it treats as a JSP page, whether the file is in the usual syntax or in XML syntax. Section 6.2.1 gives three rules and a strict order of precedence.

Table 2. How the container decides that a file is a JSP document (first matching rule wins)
Rank Rule Bookshelf example

1

A <jsp-property-group> that sets <is-xml> to true or false for the file decides, overriding everything else

/WEB-INF/views/legacy.jspx forced to the usual syntax with false; *.xhtml forced to XML with true

2

Otherwise, with a Servlet 2.4 or later web.xml, the extension .jspx makes the file a JSP document

books-xml.jspx needs no configuration at all

3

Otherwise, if the file is a JSP page and its top element is <jsp:root>, it is a JSP document (JSP 1.2 compatibility)

A file named export.jsp that starts with jsp:root

If a file is identified as a JSP document and is not well-formed and namespace-aware XML, translation fails. Nothing is sent to the client except the 500 error page.

flowchart TD start["A file the container treats as a JSP page"] --> pg{"A jsp-property-group sets is-xml for it?"} pg -->|"true"| xml["Parse as a JSP document"] pg -->|"false"| std["Parse in the usual JSP syntax"] pg -->|"no group says"| ext{"Extension is .jspx?"} ext -->|"yes"| xml ext -->|"no"| root{"Top element is jsp:root?"} root -->|"yes"| xml root -->|"no"| std xml --> wf{"Well-formed and namespace-aware?"} wf -->|"no"| err["Translation error: HTTP 500 JasperException"] wf -->|"yes"| view["Interpret as a JSP page, then expose the XML view to tag library validators"]

The web.xml below belongs to the Bookshelf of this page (property groups are covered on the Configuration and Property Groups page). The first group switches one file back to the usual syntax even though its extension is .jspx, which is the example the specification itself uses for false; the second makes every .xhtml file a JSP document. Both use the Servlet 6.1 descriptor.

The jsp-config of the Bookshelf: is-xml per file or per extension
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_1.xsd"
         version="6.1">
  <jsp-config>
    <jsp-property-group>
      <url-pattern>/WEB-INF/views/legacy.jspx</url-pattern>
      <is-xml>false</is-xml>
    </jsp-property-group>
    <jsp-property-group>
      <url-pattern>*.xhtml</url-pattern>
      <is-xml>true</is-xml>
    </jsp-property-group>
  </jsp-config>
</web-app>

A .xhtml file that is not a valid JSP page in the usual syntax shows the effect. books.xhtml is a JSP document only because of the second group:

books.xhtml: no jsp:root, no .jspx; the property group makes it a JSP document
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:c="jakarta.tags.core">
  <body><c:forEach var="b" items="${books}"><p>${b.title}</p></c:forEach></body>
</html>
Output of GET /books?v=books.xhtml (Content-Type: text/xml;charset=UTF-8)
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Dune</p><p>Neuromancer</p></body></html>

legacy.jspx contains an unclosed <br> and a taglib directive, both illegal in XML. With <is-xml>false</is-xml> it is translated as a normal page and the container never runs an XML parser on it:

legacy.jspx: a .jspx file that is deliberately not a JSP document
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<html>
<body>
<c:forEach var="b" items="${books}">${b.title}<br></c:forEach>
</body>
</html>
Output: Content-Type is text/html because the usual syntax applies
<html>
<body>
Dune<br>Neuromancer<br>
</body>
</html>

A First JSP Document: the Bookshelf as XHTML

The smallest useful JSP document is a template XML element tree with one action inside. The Bookshelf servlet forwards the list of books to /WEB-INF/views/books-xml.jspx (the servlet is the one from the Getting Started page, with a parameter that selects the view for these experiments). The page below is XHTML 1.0 Strict with a DOCTYPE generated by jsp:output, a content type chosen by jsp:directive.page and the Jakarta Tags core library bound with xmlns:c.

books-xml.jspx
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:jsp="http://java.sun.com/JSP/Page"
      xmlns:c="jakarta.tags.core">
  <jsp:directive.page contentType="application/xhtml+xml; charset=UTF-8" />
  <jsp:output doctype-root-element="html"
              doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
              doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" />
  <head><title>Bookshelf</title></head>
  <body>
    <h1>Bookshelf</h1>
    <ul>
      <c:forEach var="b" items="${books}">
        <li>${b.title} by <c:out value="${b.author}"/> (${b.year})</li>
      </c:forEach>
    </ul>
  </body>
</html>
Response of GET /books?v=books-xml.jspx
HTTP/1.1 200
Content-Type: application/xhtml+xml;charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head><title>Bookshelf</title></head><body><h1>Bookshelf</h1><ul><li>Dune by Frank Herbert (1965)</li><li>Neuromancer by William Gibson (1984)</li></ul></body></html>

Several things happened that a page in the usual syntax would not do. The container added an XML declaration because there is no jsp:root. It inserted the DOCTYPE immediately before the first element, from the three jsp:output attributes. It removed every whitespace-only text node (the indentation and line breaks between elements), which is why the output is one line. The jsp prefix is bound to http://java.sun.com/JSP/Page on the root element, and the prefix itself is irrelevant: only that URI marks an element as a standard action. The jsp namespace declaration does not appear in the output, while the XHTML namespace does, because the html element is template data and is passed through.

Namespaces, Standard Actions and Tag Libraries

A page in the usual syntax imports a tag library with <%@ taglib prefix="c" uri="..." %>. A JSP document cannot, because <%@ is not XML, so the taglib directive is not allowed in a JSP document and libraries are imported with xmlns attributes. Standard actions are recognized by namespace, never by prefix.

Table 3. The three forms of the xmlns value that imports a tag library (section 6.3.1)
Form Meaning Bookshelf example

Plain URI

Looked up in the taglib map of web.xml extended with the URIs declared in the <uri> element of every packaged TLD. The JSTL 3.0 core library is registered as jakarta.tags.core

xmlns:c="jakarta.tags.core" (verified, and so is the old http://java.sun.com/jsp/jstl/core)

urn:jsptld: followed by a path

The TLD is found by the "TLD resource path" mechanism of section 7.3.2: a path inside the web application or a JAR

xmlns:xv="urn:jsptld:/WEB-INF/tlds/xmlview.tld" (verified)

urn:jsptagdir: followed by a directory

A directory of tag files, as in section 8.4

xmlns:b="urn:jsptagdir:/WEB-INF/tags" (the .tagx example below)

Four rules decide what happens to other elements.

  • Standard actions are the elements in the namespace http://java.sun.com/JSP/Page, with any prefix. In a JSP document the prefix is arbitrary but the URI is not: the specification shows wombat:root working, and shows that binding jsp to another URI turns jsp:root into ordinary template data.

  • Custom actions are the elements in the namespace of an imported library, introduced on the root element or on any descendant. Unlike JSP 1.2, not every library has to be declared on the root.

  • Template elements are everything else. They can have qualified names (and be in a namespace) or be unqualified, and they are written to the output.

  • An unknown namespace is not an error. A plain-URI namespace that matches no taglib map entry produces no translation error; its elements are treated as uninterpreted template data.

unknown-ns.jspx: a namespace the container does not know is left alone
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:x="urn:example:unknown">
  <body><x:note>kept as plain markup</x:note></body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns:x="urn:example:unknown" xmlns="http://www.w3.org/1999/xhtml"><body><x:note>kept as plain markup</x:note></body></html>

An unbound prefix is a different matter. The XML parser has no idea what c: means, so a document that uses c:forEach without an xmlns:c fails before JSP processing starts. This is the most common first error after copying a page from the usual syntax:

noxmlns.jspx and the error it raises
<html xmlns="http://www.w3.org/1999/xhtml">
  <body><c:forEach var="b" items="${books}"><p>${b.title}</p></c:forEach></body>
</html>
org.apache.jasper.JasperException: /WEB-INF/views/noxmlns.jspx (line: [2], column: [45]) The prefix "c" for element "c:forEach" is not bound.

A default namespace also changes what unqualified elements mean. If xmlns="..." pointed at a tag library, unqualified elements would become invocations of its tags, a trap the specification points out in its namespace-aware example (6.4.2). The Bookshelf pages bind XHTML as the default namespace and give every library an explicit prefix.

jsp:root

jsp:root is the optional root element of a JSP document. It was mandatory in JSP 1.2, and the specification expects most documents in Pages 4.0 not to use it. It matters for two reasons: it marks a file as a JSP document without the .jspx extension or a property group, and it lets a document produce something other than a single XML document: a sequence of XML documents, or non-XML content such as plain text. It may appear only as the root of a JSP document or of a tag file in XML syntax, or the translation fails.

Table 4. The attribute of jsp:root
Attribute Required Meaning

version

yes

The version of the specification the page uses. The container rejects other values with a translation error. Valid values are 1.2, 2.0, 2.1, 2.2, 2.3, 3.0, 3.1 and 4.0

xmlns:*

no

Namespace declarations for standard actions, custom actions or generated template text

Tomcat 11.0.26 checks the version and lists the accepted values in its message: Invalid version number: [5.0], must be "1.2", "2.0", "2.1", "2.2", "2.3", "3.0", "3.1" or "4.0" (observed with version="5.0").

The second use is easiest to see with two root-level elements. A real XML document has one root, so a document without jsp:root could not do this; with it, the two book elements are emitted as a sequence, and, by default, without an XML declaration:

root-two.jspx: two top-level elements
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="4.0">
  <book>Dune</book>
  <book>Neuromancer</book>
</jsp:root>
Output (no XML declaration)
<book>Dune</book><book>Neuromancer</book>

The same mechanism generates non-XML content. The Bookshelf export below is a semicolon-separated text file. There are no template elements, only jsp:text and an action. The line break is inside the jsp:text body, so it is kept verbatim.

export.jspx: a CSV export from a JSP document
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
          xmlns:c="jakarta.tags.core"
          version="4.0">
  <jsp:directive.page contentType="text/plain; charset=UTF-8" />
  <c:forEach var="b" items="${books}">
    <jsp:text>${b.id};${b.title};${b.author};${b.year}
</jsp:text>
  </c:forEach>
</jsp:root>
Response of GET /books?v=export.jspx
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8

1;Dune;Frank Herbert;1965
2;Neuromancer;William Gibson;1984

CSV cells that contain ;, quotes or line breaks need quoting, and a title that starts with = is interpreted as a formula when the file is opened in a spreadsheet. A real export belongs in a servlet with a CSV library; the example shows only the mechanism.

jsp:output: XML Declaration and DOCTYPE

jsp:output controls the prolog of the generated document and exists only in the XML syntax. It cannot have a body and may appear in JSP documents and in tag files in XML syntax. In a page in the usual syntax it is a translation error; Tomcat reports <jsp:output> must not be used in standard syntax (observed).

Table 5. The attributes of jsp:output
Attribute Required Meaning

omit-xml-declaration

no

true or yes suppresses the XML declaration line (<?xml version="1.0" encoding="UTF-8"?> in the examples); false or no forces it. The encoding named is the response character encoding

doctype-root-element

only with doctype-system

The name that follows <!DOCTYPE. Must appear if and only if doctype-system appears

doctype-system

no

Requests a DOCTYPE and supplies the system literal

doctype-public

no

The public identifier; only legal together with doctype-system

Table 6. Defaults of omit-xml-declaration
Where Default

JSP document with a jsp:root

yes (omitted)

JSP document without a jsp:root

no (emitted)

Tag file in XML syntax

always yes (omitted)

The DOCTYPE is determined at translation time and written just before the first output element. The format is <!DOCTYPE root PUBLIC "public" "system"> when a public identifier is present and <!DOCTYPE root SYSTEM "system"> otherwise. Repeating an attribute with a different value is a translation error. The author is responsible for the URI being valid.

Three Bookshelf variants show the declaration rules. root-decl.jspx forces the declaration on a document that has a jsp:root; noroot-omit.jspx removes it from one that does not:

root-decl.jspx and noroot-omit.jspx
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="4.0">
  <jsp:output omit-xml-declaration="no"/>
  <book>Dune</book>
</jsp:root>

<catalogue xmlns:jsp="http://java.sun.com/JSP/Page">
  <jsp:output omit-xml-declaration="true"/>
  <book>Dune</book>
</catalogue>
Their outputs
<?xml version="1.0" encoding="UTF-8"?>
<book>Dune</book>

<catalogue><book>Dune</book></catalogue>

Asking for a DOCTYPE without a system literal is rejected at translation time, as the specification demands:

doctype-bad.jspx and Tomcat’s message
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:jsp="http://java.sun.com/JSP/Page">
  <jsp:output doctype-root-element="html"/>
  <body/>
</html>
org.apache.jasper.JasperException: /WEB-INF/views/doctype-bad.jspx (line: [2], column: [44]) <jsp:output>: 'doctype-root-element' and 'doctype-system' attributes must appear together

A DOCTYPE written in the source file is something else: it makes the container validate the document (see DTD Validation and Entities). jsp:output only writes a DOCTYPE into the response.

Directives in XML Syntax

Only the page and include directives (see Directives) have XML elements that work in an ordinary JSP document; the others belong to tag files.

Table 7. Directives and their XML forms
Usual syntax XML syntax Rule

<%@ page attrs %>

<jsp:directive.page attrs/>

Must be a child of the root element. Same attributes and meaning as the page directive; its scope is the document and anything pulled in with an include directive

<%@ include file="f" %>

<jsp:directive.include file="f"/>

May appear anywhere in the document. Translation-time inclusion; the XML view expands it in place

<%@ taglib ... %>

xmlns:prefix="uri"

The taglib directive is not allowed in a JSP document, and using it is a well-formedness error

<%@ tag %>, <%@ attribute %>, <%@ variable %>

<jsp:directive.tag/>, <jsp:directive.attribute/>, <jsp:directive.variable/>

Tag files only; the XML view uses the same elements

jsp:directive.include is how a document reuses a fragment. The fragment below is itself XML with its own default namespace, which is the safe style, because the specification resets the default namespace to "" at the root of an included XML segment so that the includer’s default namespace is not inherited:

footer.jspf and include.jspx
<p xmlns="http://www.w3.org/1999/xhtml">Total: ${books.size()} books</p>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:jsp="http://java.sun.com/JSP/Page">
  <body>
    <h1>Bookshelf</h1>
    <jsp:directive.include file="/WEB-INF/views/footer.jspf" />
  </body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Bookshelf</h1><p xmlns="http://www.w3.org/1999/xhtml">Total: 2 books</p>
</body></html>

Note the redundant xmlns on the fragment’s element and the line break after it in the output (observed). For request-time inclusion of another resource, jsp:include is used exactly as in the usual syntax; it is a standard action and needs no change.

A taglib directive in a document fails at parse time, because <%@ is not legal XML:

bad-taglib.jspx and the error it raises
<html xmlns="http://www.w3.org/1999/xhtml">
  <%@ taglib prefix="c" uri="jakarta.tags.core" %>
  <body><p>Dune</p></body>
</html>
org.apache.jasper.JasperException: /WEB-INF/views/bad-taglib.jspx (line: [2], column: [4]) The content of elements must consist of well-formed character data or markup.

Scripting Elements in XML Syntax

Declarations, scriptlets and expressions have exactly one legal form in a JSP document, the XML elements. The old <% %> forms are not XML, so a scriptlet in a document is a parse error:

bad-scriptlet.jspx and the error it raises
<html xmlns="http://www.w3.org/1999/xhtml">
  <body>
    <p><% out.print("Dune"); %></p>
  </body>
</html>
org.apache.jasper.JasperException: /WEB-INF/views/bad-scriptlet.jspx (line: [3], column: [9]) The content of elements must consist of well-formed character data or markup.

The XML elements have no attributes, and their bodies hold the code. The code must itself be well-formed text, so < and & are written as &lt; and &amp; or wrapped in a CDATA section.

Table 8. Scripting elements in XML form
Usual syntax XML syntax Role

<%! ... %>

<jsp:declaration>...</jsp:declaration>

Members of the generated servlet class; shared by all requests, so they are not thread-safe state

<% ... %>

<jsp:scriptlet>...</jsp:scriptlet>

Statements in _jspService

<%= ... %>

<jsp:expression>...</jsp:expression>

A value written to the output

scripting.jspx: the three elements (legacy; see the scripting-elements page of this part)
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:jsp="http://java.sun.com/JSP/Page">
  <jsp:declaration>int visits = 0;</jsp:declaration>
  <body>
    <jsp:scriptlet>visits++; int n = ((java.util.List&lt;?&gt;) request.getAttribute("books")).size();</jsp:scriptlet>
    <p>Books: <jsp:expression>n</jsp:expression></p>
  </body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Books: 2</p></body></html>

The one place the XML syntax invents new notation is the request-time attribute value of a custom action. The usual syntax writes <c:out value="<%= expr %>"/>, which is not well-formed because of the < in the attribute. In a JSP document the same value is written value="%= expr %": without the angle brackets, and with optional white space around the expression. This notation applies to custom actions only. On a template element such as <a href="%= url %"> the text is not an expression and is passed through verbatim; the specification’s workaround is to build the element from jsp:text and jsp:expression. Modern pages do not need either: href="${url}" is an EL expression in both syntaxes.

Everything that applies to scripting in the usual syntax applies here too: <scripting-invalid>true</scripting-invalid> in a property group makes the container reject these elements, and the better replacement is EL, JSTL and tag files. The XML view section below turns the same idea into a validator that works at the level of a whole application.

Template Data, Whitespace, EL and Quoting

A page in the usual syntax passes any text it does not understand through to the response. A JSP document passes structured template data: XML elements that are not actions, after the parser has processed them, plus the content of jsp:text. The semantic model does not change; only how the data is described. The sections below follow the rules in section 6.2.3 and 6.3.9.

Whitespace

White space (space, tab, carriage return, line feed) has one rule: text nodes that contain only white space are dropped, with the single exception of text inside a jsp:text element, which is kept verbatim. That is why the XHTML example above is one line, and why a single line break survives in the CSV export (it is inside jsp:text). It also means that trim-directive-whitespaces has no effect on JSP documents; it does not need to, because the parser already did the work.

jsp:text and CDATA

jsp:text encloses template data. It has no attributes, may contain EL expressions, and cannot contain nested actions or scripting elements (a nested jsp:text is a translation error). Inside a JSP document its body must still be well-formed, so characters such as < and & are escaped or put into a CDATA section, which the container passes through unparsed. The specification gives ${foo.content > 3} as an expression that is legal in a JSP page and illegal in a document, and suggests gt. On Tomcat 11.0.26 a > in a text node was accepted (observed with bad-gt.jspx), since > is legal XML character data; a <, a && or an attribute value containing < is what fails (see Pitfalls). The word operators lt, gt, le, ge, and and eq work everywhere, so use them.

escape.jspx: EL, a backslash escape and CDATA in the same document
<html xmlns="http://www.w3.org/1999/xhtml">
  <body><p>${books[0].title} \${books[0].title} <jsp:text xmlns:jsp="http://java.sun.com/JSP/Page"><![CDATA[<b>bold</b> & ${books[0].year}]]></jsp:text></p></body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><p>Dune ${books[0].title} <b>bold</b> & 1965</p></body></html>

The first expression was evaluated, the second was escaped by the backslash and printed literally, and the CDATA content reached the response unchanged, including the expression, which was still evaluated. In template text and attribute values EL works as in the usual syntax and \$ escapes it. The only difference is the backslash: \\ is not an escape sequence in an XML attribute, so what the usual syntax would print as \2 for \\${1+1} a document prints as \${1+1}.

Entities Are Resolved, Output Is Not Re-escaped

Because the container parses the file as XML, character and entity references are decoded when the file is read, and the decoded characters are what goes to the response. The output is not an XML serializer: it does not escape them again.

amp.jspx: what the response contains for entity references in text
<p xmlns="http://www.w3.org/1999/xhtml" xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:c="jakarta.tags.core">Tom &amp; Jerry <jsp:text>Tom &amp;amp; Jerry</jsp:text> <c:out value="Tom &amp; Jerry"/> <c:out value="${books[0].title} &amp; co" escapeXml="true"/></p>
Output: the first fragment is not well-formed XML any more
<?xml version="1.0" encoding="UTF-8"?>
<p xmlns="http://www.w3.org/1999/xhtml">Tom & Jerry Tom &amp; JerryTom &amp; JerryDune &amp; co</p>

Tom &amp; Jerry in the source became a bare & in the response, which is not well-formed XML and breaks an XML consumer. To emit an entity, produce the reference itself (&amp;amp; in a jsp:text) or let an action escape it: c:out with its default escapeXml turned a & coming from the value into &amp;. The same applies to dynamic data. EL in a template text node is not escaped, so a title containing < or & produces a broken document. Escape data with c:out or the fn:escapeXml function.

Dynamic Elements and Attributes

When the name of an element or the value of an attribute has to be computed, the usual standard actions apply: jsp:element, jsp:attribute and jsp:body are standard actions that exist in both syntaxes and are the most flexible mechanism the document syntax has for dynamic structure. The Bookshelf headings below use h2 for books older than 1970 and h3 for the rest, with a generated id:

element.jspx
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:jsp="http://java.sun.com/JSP/Page" xmlns:c="jakarta.tags.core">
  <body>
    <c:forEach var="b" items="${books}">
      <jsp:element name="${b.year lt 1970 ? 'h2' : 'h3'}">
        <jsp:attribute name="id">book-${b.id}</jsp:attribute>
        <jsp:body>${b.title}</jsp:body>
      </jsp:element>
    </c:forEach>
  </body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><h2 id="book-1">Dune</h2><h3 id="book-2">Neuromancer</h3></body></html>

For simple substitution no action is needed: <row>${table.value}</row> and indent="${table.indent}" are legal in a document, with the expression in the body or in an attribute.

Encoding, Content Type and the XML Declaration

JSP documents use XML’s own encoding rules instead of the usual-syntax page-encoding rules. The specification’s appendix B says the page encoding of a JSP document is determined as in appendix F.1 of XML 1.0 (byte-order mark or the encoding pseudo-attribute of the XML declaration, UTF-8 by default), and the file is read with that encoding. There is no pageEncoding to maintain for a document in XML syntax.

latin1.jspx, saved in ISO-8859-1, and what the response contains
<?xml version="1.0" encoding="ISO-8859-1"?>
<p xmlns="http://www.w3.org/1999/xhtml">Gabriel García Márquez</p>
HTTP/1.1 200
Content-Type: text/xml;charset=UTF-8

<?xml version="1.0" encoding="UTF-8"?>
<p xmlns="http://www.w3.org/1999/xhtml">Gabriel García Márquez</p>

The source was read as Latin-1 because its declaration says so, and the response is re-encoded in UTF-8; the declaration the container generated names the response encoding, not the source encoding. The same is true of the content type. Without a contentType attribute the initial response content type is text/html for pages in the usual syntax and text/xml for JSP documents, and the initial response encoding for documents is UTF-8 (section 4.2). All the Bookshelf pages above show text/xml;charset=UTF-8 for that reason. XHTML served as text/xml is rendered as XML, not as an HTML page; set application/xhtml+xml with jsp:directive.page (as books-xml.jspx does) if the browser should treat it as XHTML, or use the default-content-type property of a property group for a set of pages.

The XML view character encoding (the general rules are on the Character Encoding page), the encoding used when the view is externalised to a validator, is always UTF-8.

DTD Validation and Entities

A JSP document that has a DOCTYPE declaration must be validated by the container during translation, and failures are translation errors like any other. Pages 4.0 requires only DTD validation; containers should not validate against other schema languages. This is also how a document gets custom entities: an internal DTD subset defines them. The catch is that validation also applies to every element and attribute. A DOCTYPE without declarations for the document’s elements, and for the xmlns attributes, fails. The specification warns that a DTD must make explicit provision for the jsp prefix and its xmlns:jsp attribute if the document is going to use JSP elements.

entity.jspx: an internal DTD subset with an entity, an element declaration and the xmlns attribute
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE p [
  <!ELEMENT p ANY>
  <!ATTLIST p xmlns CDATA #FIXED "http://www.w3.org/1999/xhtml">
  <!ENTITY author "Frank Herbert">
]>
<p xmlns="http://www.w3.org/1999/xhtml">&author; wrote Dune &amp; others</p>
Output of GET /books?v=entity.jspx
<?xml version="1.0" encoding="UTF-8"?>
<p xmlns="http://www.w3.org/1999/xhtml">Frank Herbert wrote Dune & others</p>

The same file with <!DOCTYPE p [ <!ENTITY author "Frank Herbert"> ]> and nothing else fails with Element type "p" must be declared. (observed), because the presence of a DOCTYPE turns validation on. Prefer a document with no DOCTYPE and put the DOCTYPE in the output with jsp:output. The response then contains the identifier but the container does not try to fetch and validate against the DTD.

Tag Files in XML Syntax

The same rules apply to tag files. A tag file in XML syntax has the extension .tagx, a .tag file is never in XML syntax, and jsp:root can (but need not) appear in a .tagx and cannot appear in a .tag. The tag directives become jsp:directive.tag, jsp:directive.attribute and jsp:directive.variable. Two differences from JSP documents: the default of omit-xml-declaration is always yes, and the tag directive cannot have a pageEncoding attribute (the encoding comes from XML); using it is a translation-time error.

WEB-INF/tags/bookrow.tagx and the document that uses it
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="4.0">
  <jsp:directive.tag body-content="empty" />
  <jsp:directive.attribute name="book" required="true" type="org.example.bookshelf.Book" />
  <li xmlns="http://www.w3.org/1999/xhtml">${book.title} (${book.year})</li>
</jsp:root>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:c="jakarta.tags.core" xmlns:b="urn:jsptagdir:/WEB-INF/tags">
  <body><ul><c:forEach var="x" items="${books}"><b:bookrow book="${x}"/></c:forEach></ul></body>
</html>
Output
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"><body><ul><li xmlns="http://www.w3.org/1999/xhtml">Dune (1965)</li><li xmlns="http://www.w3.org/1999/xhtml">Neuromancer (1984)</li></ul></body></html>

Tag files live under /WEB-INF/tags (or /META-INF/tags in a JAR), and the directory in urn:jsptagdir: and the document needs no taglib directive. Tag files in general, with .tag syntax, are the subject of the tag-files page of this part.

The XML View

Every JSP page, in either syntax, has an XML view: an XML document derived from it by a fixed mapping. The container does not run the page from it. Its job is validation. When a tag library declares a <validator> in its TLD, the container hands the validator a jakarta.servlet.jsp.tagext.PageData object whose getInputStream() returns the XML view, and the validator returns ValidationMessage objects for anything wrong. For a JSP document the transformation is small. For a page in the usual syntax it is a full rewrite.

flowchart LR subgraph T["Translation phase"] A["books.jsp (usual syntax)"] --> XV B["books-xml.jspx (JSP document)"] --> XV XV["XML view: one jsp:root, jsp:id on every element"] --> V["TagLibraryValidator.validate(prefix, uri, PageData)"] V -->|"null or empty array"| J["Generate and compile the servlet"] V -->|"ValidationMessage[]"| E["Translation error: HTTP 500"] end subgraph R["Request phase"] J --> Q["_jspService runs for each request"] end
Table 9. The transformation for a JSP document or a tag file in XML syntax (section 10.1.1)
Step What happens

1

Include directives are expanded in place (so the view of a document that includes a usual-syntax fragment contains that fragment’s elements as well)

2

A jsp:root is added if the document does not have one

3

The pageEncoding attribute of the page directive is set to UTF-8; the directive and attribute are added if missing

4

The contentType attribute is set to the value the container passes to ServletResponse.setContentType(); added if missing

5

A jsp:id attribute is added to every element

Table 10. The transformation for a page in the usual syntax (section 10.1.2, Table JSP.10-1)
Usual syntax XML view

<%-- comment --%>

Removed

<%@ page ... %>

<jsp:directive.page .../> with jsp:id

<%@ taglib ... %>

An xmlns:prefix on the jsp:root. A relative uri becomes urn:jsptld: followed by the path; otherwise the uri is used as is. tagdir becomes urn:jsptagdir: followed by the directory

<%@ include ... %>

Expanded in place

<%! ... %>, <% ... %>, <%= ... %>

jsp:declaration, jsp:scriptlet, jsp:expression

Standard action

XML syntax, request-time attributes converted

Custom action

Unchanged, request-time attributes converted

Template text

A jsp:text element, for every run of text

Because a page in the usual syntax has all of its template text collapsed into jsp:text elements, a validator sees no detail about its HTML. A JSP document keeps its template elements visible, so a validator (or an XSLT transformation) can check, for example, that a page contains exactly one h1. That is the actual advantage of writing in XML.

jsp:id is an attribute that the container must support on every element of the view, in the namespace of the JSP standard actions with any prefix that the container picks, and it is unique within a view. A validator puts it in ValidationMessage.getId() so the container can map the problem back to a location. It is not available to page authors.

Table 11. Escaped EL in the XML view (section 10.1.11, Table JSP.10-2, abridged)
Attribute value in the page XML view Result when ${foo} is [bar]

${foo}

${foo}

[bar]

\${foo}

\${foo}

${foo}

\\${foo}

${'\\'}${foo}

\[bar]

Seeing the XML View: a Validator That Dumps It

The best way to understand the mapping is to look at it. The validator below writes the XML view of every page that imports its library to a file; it accepts every page by returning null. It is a debugging aid only: no production code should write files from a validator.

XmlViewDumper.java
package org.example.bookshelf;

import jakarta.servlet.jsp.tagext.PageData;
import jakarta.servlet.jsp.tagext.TagLibraryValidator;
import jakarta.servlet.jsp.tagext.ValidationMessage;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class XmlViewDumper extends TagLibraryValidator {
    @Override
    public ValidationMessage[] validate(String prefix, String uri, PageData page) {
        try (InputStream in = page.getInputStream()) {
            String xml = new String(in.readAllBytes(), StandardCharsets.UTF_8);
            Files.writeString(Path.of(System.getProperty("java.io.tmpdir"), "xmlview-" + prefix + ".xml"), xml);
        } catch (IOException e) {
            return new ValidationMessage[] {new ValidationMessage(null, e.toString())};
        }
        return null;
    }
}
WEB-INF/tlds/xmlview.tld: a TLD that only declares the validator
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="https://jakarta.ee/xml/ns/jakartaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-jsptaglibrary_4_0.xsd"
        version="4.0">
  <tlib-version>1.0</tlib-version>
  <short-name>xv</short-name>
  <uri>https://example.org/bookshelf/xmlview</uri>
  <validator>
    <validator-class>org.example.bookshelf.XmlViewDumper</validator-class>
  </validator>
</taglib>

A page opts in by importing the library, with a taglib directive in the usual syntax or with xmlns:xv in a document. The XML view of books-xml.jspx (the document from the start of the page, without the jsp:output) is:

The XML view of a JSP document (Tomcat 11.0.26, first lines and the loop)
<?xml version="1.0" encoding="UTF-8" ?>
<jsp:root
  version="2.0"
  xmlns:jsp="http://java.sun.com/JSP/Page"
  jsp:id="0"
>
<jsp:directive.page
  jsp:id="1"
  pageEncoding="UTF-8"
  contentType="application/xhtml+xml; charset=UTF-8"
/>
<html
  xmlns:xv="https://example.org/bookshelf/xmlview"
  xmlns:c="jakarta.tags.core"
  xmlns:jsp="http://java.sun.com/JSP/Page"
  xmlns="http://www.w3.org/1999/xhtml"
  jsp:id="2"
>
...
<c:forEach
  items="${books}"
  var="b"
  jsp:id="8"
>
<li
  jsp:id="9"
>
${b.title}
...
</li>
</c:forEach>
...
</html>
</jsp:root>

The jsp:root, the jsp:id attributes and the forced pageEncoding="UTF-8" are the steps of the transformation, and the template elements (html, head, body, li) are still elements. Compare the XML view of the usual-syntax books.jsp of the Getting Started page, which imports the same library:

The XML view of the same list written in the usual syntax (first part and the loop)
<?xml version="1.0" encoding="UTF-8" ?>
<jsp:root
  version="2.0"
  xmlns:c="jakarta.tags.core"
  xmlns:xv="https://example.org/bookshelf/xmlview"
  xmlns:jsp="http://java.sun.com/JSP/Page"
  jsp:id="0"
>
<jsp:directive.page
  jsp:id="1"
  pageEncoding="UTF-8"
  contentType="text/html; charset=UTF-8"
/>
<jsp:text
  jsp:id="2"
>
<![CDATA[

]]>
</jsp:text>
...
<jsp:text
  jsp:id="7"
>
<![CDATA[
<body>
]]>
</jsp:text>
...
<c:forEach
  var="b"
  items="${books}"
  jsp:id="11"
>
...
<jsp:text jsp:id="14">${b.title}</jsp:text>
...
<c:out
  value="${b.author}"
  jsp:id="16"
/>
...
</c:forEach>
...
</jsp:root>

The two taglib directives became xmlns attributes on the root, each line of the HTML became a jsp:text, the EL expression got its own jsp:text, and the comment is gone. Two things in Tomcat 11.0.26’s views differ from the specification text: the root says version="2.0" although section 10 does not fix a number, and the jsp:output of the document is not in its view (it is consumed to generate the prolog). Treat the view as an implementation of the mapping, not as a byte-exact format. Validators should parse it with a namespace-aware XML parser and not with string matching.

The Bookshelf Worked Example: Forbidding Scripting in the Whole Application

The scripting-invalid property group turns scripting off for a set of files, one URL pattern at a time. The XML view lets an application go further and enforce its own rule in one place, with an error message of its own and with a line reference. The validator below parses the view with a namespace-aware SAX parser and reports every jsp:declaration, jsp:scriptlet and jsp:expression, whichever syntax the page used. The validator reports the container-assigned jsp:id as the location.

NoScriptingValidator.java
package org.example.bookshelf;

import jakarta.servlet.jsp.tagext.PageData;
import jakarta.servlet.jsp.tagext.TagLibraryValidator;
import jakarta.servlet.jsp.tagext.ValidationMessage;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;

public class NoScriptingValidator extends TagLibraryValidator {
    private static final String JSP_NS = "http://java.sun.com/JSP/Page";

    @Override
    public ValidationMessage[] validate(String prefix, String uri, PageData page) {
        List<ValidationMessage> problems = new ArrayList<>();
        try (InputStream in = page.getInputStream()) {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            factory.setNamespaceAware(true);
            factory.newSAXParser().parse(in, new DefaultHandler() {
                @Override
                public void startElement(String ns, String local, String qName, Attributes atts) {
                    if (JSP_NS.equals(ns)
                            && (local.equals("scriptlet") || local.equals("expression") || local.equals("declaration"))) {
                        problems.add(new ValidationMessage(atts.getValue(JSP_NS, "id"),
                                "Scripting element jsp:" + local + " is not allowed"));
                    }
                }
            });
        } catch (Exception e) {
            problems.add(new ValidationMessage(null, e.toString()));
        }
        return problems.isEmpty() ? null : problems.toArray(new ValidationMessage[0]);
    }
}

The TLD is the same shape as xmlview.tld, with the class name replaced and <uri>https://example.org/bookshelf/no-scripting</uri>;. A clean document and two offenders, one in each syntax, show the effect:

validated-ok.jspx, validated-bad.jspx and validated-bad.jsp
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:c="jakarta.tags.core"
      xmlns:ns="https://example.org/bookshelf/no-scripting">
  <body><c:forEach var="b" items="${books}"><p>${b.title}</p></c:forEach></body>
</html>

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:jsp="http://java.sun.com/JSP/Page"
      xmlns:ns="https://example.org/bookshelf/no-scripting">
  <body><p><jsp:expression>1 + 1</jsp:expression></p></body>
</html>

<%@ taglib prefix="ns" uri="https://example.org/bookshelf/no-scripting" %>
<html><body><p><%= 1 + 1 %></p></body></html>
Results
validated-ok.jspx   200  <html xmlns="http://www.w3.org/1999/xhtml"><body><p>Dune</p><p>Neuromancer</p></body></html>
validated-bad.jspx  500  Validation error messages from TagLibraryValidator for [ns] in [/WEB-INF/views/validated-bad.jspx]: 5: Scripting element jsp:expression is not allowed
validated-bad.jsp   500  Validation error messages from TagLibraryValidator for [ns] in [/WEB-INF/views/validated-bad.jsp]: 6: Scripting element jsp:expression is not allowed

Tomcat printed the jsp:id (5 and 6) as the location, and rejected the page when it was translated, so the error appears on the first request of a page, or at precompilation. The rule applies to a page only if the page imports the library, so a real application would add the library to every view through a prelude in a property group (include-prelude). The parsing uses plain JAXP, and the same view could be handed to an XML Schema validator or an XSLT stylesheet.

Security Notes

  • Escape dynamic data. A JSP document is not an XML serializer. EL in text or in an attribute is inserted as is, so a title such as </p><script> breaks the structure and can inject markup, exactly as in the usual syntax. Use c:out, which escapes by default, or fn:escapeXml around every value that is not trusted, in text and in attributes.

  • A broken document is a denial of service for the XML consumer. Unescaped & and < make the output ill-formed, and a client that insists on well-formed XML shows an error instead of the page.

  • Keep documents under WEB-INF. Same rule as for pages in the usual syntax: a .jspx under the web root can be requested directly; a view under /WEB-INF/views/ can only be reached through the servlet.

  • DOCTYPE and entities. Leave the DOCTYPE out of source files unless a trusted internal subset is needed. A DOCTYPE turns on validation and entity resolution in the container’s parser, and any external entity is a way for a document to make the server read a resource. Documents are written by developers, not users, so the risk is a supply-chain one: review .jspx files from third parties as code.

  • Scripting. jsp:scriptlet is Java code, and the security properties of scriptlets are those of the usual syntax; see the validator above.

Pitfalls and Error Names

Table 12. JSP document pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom or error Cause Fix

JasperException ... The content of elements must consist of well-formed character data or markup (observed)

A <% %> scriptlet or a taglib directive in a document

Use jsp:scriptlet or, better, EL and JSTL; import the library with xmlns

JasperException ... The prefix "c" for element "c:forEach" is not bound (observed)

The document uses a prefix without an xmlns declaration

Declare xmlns:c="jakarta.tags.core" on the root or on an ancestor

The element type "br" must be terminated by the matching end-tag (observed)

An HTML-style element that is not closed

Write <br/>; every element needs an end tag or a self-closing form

The content of elements must consist of well-formed character data or markup, or The value of attribute ... must not contain the '<' character (observed with < and && in an EL expression in text, and with < in an attribute)

A < or && inside EL in a text node or attribute

Use lt, gt, le, ge, and, or (${b.year lt 1970 and b.id eq 1}); or write &amp;&amp;

Output contains & or < where the source had &amp; or &lt; (observed)

Entities are decoded at parse time and the output is not re-escaped

Emit &amp;amp; through jsp:text, or use c:out for data

Element type "p" must be declared (observed)

A source DOCTYPE turned on validation, and the DTD does not declare the elements or the xmlns attribute

Remove the DOCTYPE and generate one with jsp:output, or declare everything

<jsp:output> must not be used in standard syntax (observed)

jsp:output in a file that is not a JSP document

Rename the file to .jspx or set is-xml

Invalid version number: [5.0] (observed)

jsp:root version is not one of the accepted versions

Use 4.0

'doctype-root-element' and 'doctype-system' attributes must appear together (observed)

Incomplete jsp:output

Supply the system literal (and the root element)

Browser shows a tree of XML instead of the page

The default content type of a JSP document is text/xml

jsp:directive.page contentType="application/xhtml+xml; charset=UTF-8", or default-content-type in a property group

Expected whitespace is missing

Whitespace-only text nodes are dropped

Put it in jsp:text

A page named *.jspx is parsed as usual JSP, or the other way round

A property group with is-xml overrides the extension

Check jsp-config first; rank 1 beats rank 2

Unknown tag silently printed as markup (observed)

The xmlns URI is not in the taglib map, so the elements are template data

Check the URI against the TLD <uri> or the taglib mapping in web.xml

What Changed Recently

Table 13. Changes that affect JSP documents and the XML view
Release Change

JSP 1.2

jsp:root mandatory, with every tag library declared on it

JSP 2.0

jsp:root optional; libraries may be declared on any element; .jspx default extension; is-xml in property groups; urn:jsptagdir: and tag files in XML syntax (.tagx)

JSP 2.1

The sequence #{ is reserved for EL expressions, in documents as in pages

Jakarta Pages 3.0 (EE 9)

Namespace move from javax.servlet.jsp to jakarta.servlet.jsp; the jsp namespace URI http://java.sun.com/JSP/Page did not change

Jakarta Pages 3.1

jsp:plugin deprecated; option to raise PropertyNotFoundException for unknown EL identifiers

Jakarta Pages 4.0 (EE 11)

Java 17 minimum; jsp:plugin and related actions, isThreadSafe, BodyTag.EVAL_BODY_TAG removed; the XML syntax is unchanged. ErrorData gets the query string and the method

Jakarta Tags 3.0

The core library URI is jakarta.tags.core; the http://java.sun.com/jsp/jstl/core URI is still accepted as an xmlns value (observed on Tomcat 11.0.26)

The XML syntax itself has been stable since JSP 2.0. Section 6.5 of the specification, a non-normative section, still lists two wished-for features that nobody has implemented: producing XML natively (a node tree instead of text) and XML Schema and XInclude support.

Where the Books Are Outdated

Web Development with JavaServer Pages (Manning, 2001, listed in the section bibliography) treats JSP documents and the XML view in its chapter on validating custom tag libraries, and JSTL in Action (Manning, 2003) spends its early chapters on the XML rules that JSP syntax follows. Both were written for JSP 1.2.

Table 14. JSP 1.2-era material and its equivalent today
The book’s era Today (Pages 4.0)

jsp:root is mandatory and has to declare every tag library with xmlns

Optional; libraries can be declared where they are used; version accepts 1.2 to 4.0

XML syntax is an alternative that most containers did not complete

Part of the specification, with .jspx, is-xml, .tagx and urn:jsptagdir:

Taglib URIs such as http://java.sun.com/jstl/core and _rt twin libraries

jakarta.tags.core and the other jakarta.tags.* URIs; the old http://java.sun.com/jsp/jstl/* form is only an identifier and a dead link

TagLibraryValidator over a JSP 1.2 XML view

Same API, in jakarta.servlet.jsp.tagext; the view now has jsp:id on every element and expands includes in place

javax.servlet.jsp.*

jakarta.servlet.jsp.* since Jakarta EE 9; Tomcat 11 does not run the old names

TLDs with a JSP 1.2 DTD

XML Schema TLDs (web-jsptaglibrary_4_0.xsd)

Generating XML with scriptlets and out.println

Documents generate XML directly, and JSTL and tag files cover the rest

XHTML delivered with the usual syntax and a text/html type

application/xhtml+xml through jsp:directive.page; HTML5 generally does not need the XML syntax

References