Syntax, Comments & Escaping

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), 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 sections 1.3, 1.5, 1.6, 3.3, 5.12, 6.2 and 10.1.3 of the Pages 4.0 specification and its change log, and every example by building it into a Maven WAR (context path /g3syntax) and requesting it with curl on Tomcat 11.0.26. The outputs shown are the outputs that run produced; lines that end with a dollar sign come from curl | cat -e, where the sign marks the end of the line. A JSP file is a mixture of two things, text that the container copies to the response untouched and elements that it executes, and almost every syntax question comes down to telling the translator which is which. The running scenario is the Bookshelf: a servlet context listener puts a list of three books (Dune, Emma, Ulysses) and a shop name into application scope, and each example is a small page that displays them.

Elements and Template Text

The specification defines a page as a sequence of elements and template data (usually called template text). An element is an instance of an element type that the container knows; template text is everything else, "anything that the JSP translator does not know about". The translator copies template text to the response, in order, and replaces each element with whatever that element produces. Nothing in template text is escaped, interpreted or validated. It can be HTML, XML, CSV, JSON or a plain-text mail, which is why the same technology serves all of them.

There are three element types, and each has its own syntax.

Table 1. The element types of a JSP page in standard syntax (Pages 4.0, section 1.3.2)
Element type Syntax When it acts Example

Directive

<%@ directive attr="value" %>

Translation time; applies to the whole page, produces no output

<%@ page contentType="text/html" %>

Action (standard or custom)

<prefix:name attr="value">body</prefix:name> or the empty form <prefix:name attr="value"/>

Request time; the standard ones use the prefix jsp

<jsp:include page="footer.jsp"/>, <c:forEach ...>

Scripting element

<%! declaration %>, <% scriptlet %>, <%= expression %>

Request time (a declaration at class level); Java code glued around the template text

<%= 1 + 1 %>

Expression Language

${expr} (immediate) and #{expr} (deferred)

Request time; allowed in template text and in attribute values that accept expressions

${books[0].title}

The first example is the smallest useful Bookshelf page. It contains one directive (page), one directive (taglib), one standard-library action (c:forEach), EL expressions in template text, one scripting expression and a lot of plain HTML template text.

elements.jsp: template text mixed with a directive, an action, EL and a scripting expression
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<h1>Bookshelf</h1>
<ul>
<c:forEach var="b" items="${books}">
  <li>${b.title} (${b.year})</li>
</c:forEach>
</ul>
<p>Generated at <%= 1 + 1 %> o'clock</p>

A request for /g3syntax/elements.jsp returned HTTP 200 with the body below.

Response of elements.jsp
$
$
<h1>Bookshelf</h1>$
<ul>$
$
  <li>Dune (1965)</li>$
$
  <li>Emma (1815)</li>$
$
  <li>Ulysses (1922)</li>$
$
</ul>$
<p>Generated at 2 o'clock</p>$

Three things are visible in that output. The two directives produced no text of their own, but the line breaks after them are template text and were copied (the first two empty lines). The c:forEach body ran three times, and every iteration copied the line break before and after <li> as well, which explains the blank lines between items. Finally ${b.title} and <%= 1 + 1 %> were replaced by their values. The whitespace section below shows how to control those blank lines.

The translator decides what it is looking at by the first characters of each construct and applies the first rule that matches (the specification calls this the greedy algorithm and gives the full EBNF in section 1.3.10). The rules, in the order of the grammar, are summarised in the figure.

flowchart TD A["Translator reads the next characters of the page"] --> B{"Which opener comes first?"} B -->|"JSP comment opener"| C["JSP comment: skipped, no output"] B -->|"<%@"| D["Directive: acts at translation time"] B -->|"<%!   <%   <%="| E["Scripting element: Java code"] B -->|"$ or # followed by {"| F["EL expression: evaluated at request time"] B -->|"<jsp:name"| G["Standard action"] B -->|"<prefix:name"| H["Custom action: needs a taglib directive"] B -->|"anything else"| I["Template text: copied to the response, HTML comments included"]

The figure shows the decision the translator makes for every position in the file, and the last row is the important one: an HTML comment, an unknown tag such as <br>, a <script> block or a JSON document is simply template text. Two consequences follow. The container never validates your HTML, so a page can be malformed HTML or even not XML at all (<br> without a closing slash works), and a tag whose prefix was never declared is not reported either. With a taglib directive for c, a misspelled <c:foreach> is an error, but <cc:forEach var="b" items="${books}"> with an undeclared prefix is template text that goes to the browser, and the ${books} inside its attribute is still evaluated (the request for prefix.jsp returned the whole list printed inside the attribute). The pitfalls table repeats the point.

br.jsp: not well-formed XML is fine in standard syntax
<%@ page contentType="text/html; charset=UTF-8" %>
<p>Not well-formed XML<br>but fine as a JSP page &nbsp; ${books[1].title}

The response was <p>Not well-formed XML<br>but fine as a JSP page   Emma, with the entity and the unclosed paragraph untouched. A JSP document, covered below, is the opposite case: the file is parsed as XML first and must be well-formed.

Scoped Variables in Template Text

An EL expression in template text, such as ${shopName}, names a scoped variable: an attribute stored in one of the four scopes of a page, page, request, session and application. A bare name is looked up in that order, and the first scope that holds it wins; a prefixed name (${requestScope.label}, ${applicationScope.shopName}) reads one scope only. The page below stores a variable called label in all four scopes with c:set, prints the bare name, prints the four prefixed forms, removes the page-scope copy with c:remove and prints the bare name again. The full rules for scopes, the session attribute of the page directive and the lifetime of each scope are on Implicit Objects & Scopes.

scopes.jsp: one name in four scopes
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:set var="label" value="from page scope" scope="page"/>
<c:set var="label" value="from request scope" scope="request"/>
<c:set var="label" value="from session scope" scope="session"/>
<c:set var="label" value="from application scope" scope="application"/>
<p>${label}</p>
<p>${pageScope.label} | ${requestScope.label} | ${sessionScope.label} | ${applicationScope.label}</p>
<c:remove var="label" scope="page"/>
<p>${label}</p>
<p>${shopName}</p>
Response of scopes.jsp (blank lines left by the directive and the c:set actions are omitted)
<p>from page scope</p>
<p>from page scope | from request scope | from session scope | from application scope</p>
<p>from request scope</p>
<p>Bookshelf "Corner" & Co.</p>

The bare name found the page-scope copy first, and found the request-scope copy once the page-scope copy was gone (observed on Tomcat 11.0.26). The last line shows the shop name stored by the context listener in application scope, found by the bare name because no narrower scope holds shopName. Because a bare name can be shadowed by a narrower scope, a variable that matters (a user, a permission, a flag) is better read with its scope prefix.

Start Tags, End Tags, Names and Attribute Values

The rest of section 1.3 is a short list of rules inherited from XML. They explain most of the translation errors a beginner meets.

Table 2. Syntax rules for elements (Pages 4.0, sections 1.3.3 to 1.3.7)
Rule Meaning Error when broken

Start and end in the same file

An element with a start tag and an end tag, and a scripting element such as <% ... %>, must open and close in the same physical file. A fragment brought in with the include directive cannot supply the closing %> of an element that the including page opened

Unterminated [<%] tag

Empty elements

<x:foo></x:foo>, <x:foo/> and <x:foo><%-- note --%></x:foo> are all the same empty element; a body of only a JSP comment is still empty, a body with a space or an HTML comment is not

none, but the body is executed when it is not empty

Case-sensitive names

Tag names and attribute names are case-sensitive as in XML: c:forEach, not c:foreach

No tag [Out] defined in tag library imported with prefix [c]

Quoted attribute values

Attribute values are always quoted, with single or double quotes, and the same quoting rules apply to literals and to request-time expressions

a translation error naming the attribute value

One value per attribute

An attribute may be given either as an XML attribute or with jsp:attribute, never both

The attribute [value] specified in the standard or custom action also appears as the value of the name attribute in the enclosed jsp:attribute

Names

Action names must be XML names (NMTOKEN), attribute names follow the JavaBeans conventions, and names that start with jsp, _jsp, java or jakarta are reserved

depends on the name

The jsp:attribute and jsp:body actions deserve a short example, because they are the one place where a page passes markup as an attribute value. Both pages below were written to fail, so that the message is recorded. The first sets value twice, the second spells the tag name with a capital letter.

err-attrdup.jsp and err-case.jsp: two translation errors
<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:out value="a">
  <jsp:attribute name="value">b</jsp:attribute>
</c:out>

<%-- err-case.jsp, line 3 --%>
<c:Out value="x"/>

Tomcat answered HTTP 500 with a JasperException. The message of the first was /err-attrdup.jsp (line: [3], column: [0]) The attribute [value] specified in the standard or custom action also appears as the value of the name attribute in the enclosed jsp:attribute; the message of the second was /err-case.jsp (line: [3], column: [0]) No tag [Out] defined in tag library imported with prefix [c]. The line and column point at the start tag of the element, and the message is produced at translation time, so no output of the page is sent when it appears.

A scripting element must be closed in the file that opened it. The page below includes a fragment that opens a scriptlet and never closes it, hoping that the including page supplies the %>.

err-split-file.jsp and the fragment WEB-INF/fragments/open.jspf
<%@ page contentType="text/plain" %>
<%@ include file="/WEB-INF/fragments/open.jspf" %> %>

<%-- WEB-INF/fragments/open.jspf contains: --%>
<% if (true) {

The message was /err-split-file.jsp (line: [2], column: [0]) /WEB-INF/fragments/open.jspf (line: [1], column: [3]) Unterminated [<%] tag: Jasper names the including page and the fragment, with the position inside the fragment. The same message, with the page itself in place of the fragment, appears for a file that ends in the middle of a scriptlet (err-unclosed.jsp).

Standard Syntax and XML Syntax

Every element of the table above has a second spelling in XML syntax. The XML spellings exist so that a page can be well-formed XML (a JSP document, normally in a .jspx file) and can therefore be processed by XML tools. They are also legal inside an ordinary .jsp file in standard syntax, so a page can mix the two, although mixing is rarely a good idea.

Table 3. Standard syntax and its XML equivalent (Pages 4.0, sections 1.3.2, 5.12 and 6.3)
Element Standard syntax XML syntax

Page directive

<%@ page contentType="text/plain" %>

<jsp:directive.page contentType="text/plain"/>

Include directive

<%@ include file="f.jspf" %>

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

Taglib directive

<%@ taglib prefix="c" uri="jakarta.tags.core" %>

no directive: an xmlns:c="jakarta.tags.core" declaration on any element (a taglib directive cannot appear in a JSP document)

Declaration

<%! int hits; %>

<jsp:declaration>int hits;</jsp:declaration>

Scriptlet

<% hits+; %>+

<jsp:scriptlet>hits+;</jsp:scriptlet>+

Expression

<%= hits %>

<jsp:expression>hits</jsp:expression>

Request-time attribute value

value="<%= hits %>"

value="%= hits %" (custom actions only)

Verbatim template text

ordinary text

<jsp:text>text</jsp:text>, optionally wrapping a CDATA section

EL expression

${expr}

${expr} (the same)

JSP comment

<%-- note --%>

an XML comment, which is dropped

The next page uses only the XML spellings in a .jsp file. It declares a counter, increments it, prints it and then writes template text with jsp:text, where the entity < is the XML way to write a less-than sign.

xml-syntax.jsp: XML forms of the scripting elements and jsp:text
<jsp:directive.page contentType="text/plain; charset=UTF-8"/>
<jsp:declaration>private int hits = 0;</jsp:declaration>
<jsp:scriptlet>hits++;</jsp:scriptlet>
Hits: <jsp:expression>hits</jsp:expression>
<jsp:text>A &lt; B and ${books[0].title}</jsp:text>

The response was three empty lines (the line breaks after the directive, the declaration and the scriptlet) followed by Hits: 1 and A < B and Dune. The entity was not decoded in the output: jsp:text passes its body through as template data, and in a .jsp file in standard syntax the body is not run through an XML parser. The same jsp:text line in a JSP document (text.jspx, the same three directive and text lines inside a root element) returned A < B and Dune, because the XML parser decodes the entity first. This is one of the differences between the two worlds, and the JSP Documents (XML Syntax) treats the XML side fully. The next example is a JSP document, catalogue.jspx, which produces XML.

catalogue.jspx: a JSP document that generates XML
<?xml version="1.0" encoding="UTF-8"?>
<catalogue xmlns:jsp="http://java.sun.com/JSP/Page"
           xmlns:c="jakarta.tags.core" version="2.0">
  <jsp:directive.page contentType="application/xml; charset=UTF-8"/>
  <!-- XML comment: dropped from the output -->
  <c:forEach var="b" items="${books}">
    <book year="${b.year}">${b.title}</book>
  </c:forEach>
  <jsp:text><![CDATA[<raw & text>]]></jsp:text>
  <jsp:text>\${literal} ${'${'}x}</jsp:text>
</catalogue>

The response was the following. The xmlns:jsp and xmlns:c declarations are consumed by the container and removed; the whitespace-only text nodes between the elements are dropped, so the output has no blank lines; the XML comment is gone; and the CDATA content is emitted verbatim, which means that <raw & text> is not well-formed XML in the response. The last line shows both ways to produce a literal EL delimiter.

Response of catalogue.jspx
<?xml version="1.0" encoding="UTF-8"?>$
<catalogue version="2.0"><book year="1965">Dune</book><book year="1815">Emma</book><book year="1922">Ulysses</book><raw & text>${literal} ${x}</catalogue>

The XML spelling of a request-time attribute value, value="%= hits %", applies to custom actions. Tomcat 11.0.26 also rewrites it on a template element such as <a href="%= 1 %">, producing href=" 1 ", although section 6.2.2 of the specification says that on such an element it "does not represent a request-time attribute value" and should stay as written. Do not rely on either behaviour; write ${...} in attribute values of template elements, which means the same in both syntaxes.

A JSP document must be well-formed, so the standard-syntax scriptlet markers are a parse error there. The page err-doc-scriptlet.jspx is a document with a jsp:directive.page line followed by <% int x = 1; %>; Tomcat reported /err-doc-scriptlet.jspx (line: [4], column: [4]) The content of elements must consist of well-formed character data or markup. The XML parser, not the JSP translator, raises that error.

Comments

There are two purposes for comments in a JSP page: documenting the page itself, so that the text must not reach the browser, and producing a comment in the generated document, so that it must. The specification gives each its own syntax, and the scripting language adds a third. Choosing the wrong one is the commonest comment bug, and the wrong choice usually leaks something.

Table 4. The kinds of comment (Pages 4.0, section 1.5)
Kind Syntax In the response? What is evaluated inside

JSP comment

<%-- anything but a closing marker --%>

No; also absent from the XML view

Nothing: the body is ignored completely, so elements, EL and scriptlets in it never run

HTML (or XML) comment in standard syntax

<!-- text -->

Yes, as template text

Everything: actions, scriptlets, ${...} and <%= ... %> inside it are processed, and their output goes into the comment

Java comment inside a scriptlet

<% /* note */ %> or <% // note %>

No

Nothing, but it is Java source, so the scriptlet around it runs

XML comment in a JSP document

<!-- text -->

No: "the body of the content is ignored completely"

Nothing

All three comment forms of a JSP page appear in the page below, together with a block that is commented out. The shop name in application scope is Bookshelf "Corner" & Co. on purpose, to show what happens to markup characters.

comments.jsp: HTML comment, JSP comment, Java comments and a commented-out block
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<!-- HTML comment, shop: ${shopName}, sum: <%= 20 + 22 %> -->
<%-- JSP comment: ${shopName} <%= 20 + 22 %> <c:out value="never run"/> --%>
<% /* Java comment, invisible in the output */ %>
<% // Java line comment %>
<%-- Commented-out block:
<c:forEach var="b" items="${books}">
  <li>${b.title}</li>
</c:forEach>
--%>
<p>${books[0].title}</p>
Response of comments.jsp
$
$
<!-- HTML comment, shop: Bookshelf "Corner" & Co., sum: 42 -->$
$
$
$
$
<p>Dune</p>$

Only the HTML comment survives, and it contains evaluated data: the EL expression and the scripting expression were executed and the result is in the comment. Every visitor who selects "view source" sees it. The shop name is also unescaped, because EL in template text is never HTML-escaped (the escaping section below returns to this). Two rules of thumb follow.

  • Use <%-- --%> for every comment about the page itself. An HTML comment is a published message.

  • Never put request-dependent or confidential data in an HTML comment, whether through ${...} or a scriptlet. Besides disclosing it, a value that contains the two characters that end a comment (a double dash followed by a greater-than sign) closes the comment early, and whatever follows is parsed as markup. That is a cross-site scripting hole whenever the value is controlled by a user.

A JSP comment cannot be nested. The first closing marker ends the comment, and the rest is template text:

comments-nesting.jsp: comments do not nest
<%@ page contentType="text/plain; charset=UTF-8" %>
A<%-- outer <%-- inner --%> B --%>C

The response was A B --%>C: the comment ended at the first --%>, so the text B --%> (with its leading space) and the C that follows were copied as template text. For the same reason, a block that already contains a JSP comment cannot be commented out with another JSP comment: remove the inner comment first, or delete the block and let version control keep it. A comment that is not closed at all is a translation error, Unterminated [<%--] tag, reported at the opening marker (err-nested.jsp).

Quoting and Escape Conventions

JSP has no single escape character. Section 1.6 lists a separate set of rules for each context in which the same characters mean something special, and the rules are small but easy to confuse. The table collects them. "EL enabled" means that the page evaluates EL, which is the default for a web application whose descriptor is Servlet 2.4 or later.

Table 5. Quoting rules by context (Pages 4.0, section 1.6)
Context To write this Write this Notes

Template text

a literal <%

<\%

only the opener needs quoting

Template text, EL enabled

a literal $ or # before a brace

\$, \#

not required for a $ that is not followed by {, but harmless

Inside a scripting element

a literal %>

%\>

otherwise it ends the element

Inside an EL expression

a literal ${ or #{

${'${'}

no escape character exists inside EL; use a string literal. ${'}'} is legal and prints }

Attribute value

a ' in a single-quoted value

\'

required there; a " in a double-quoted value is written \"

Attribute value

a \

\\

and \$ or \# when EL is enabled

Attribute value

a %> or a <%

%\>, <\%

same as elsewhere

Attribute value

either quote

&apos;, &quot;

the entities are also available

JSP document

anything

XML rules (&lt;, &amp;, CDATA)

JSP quoting is not applied after the XML parse

The page below exercises the template-text and scripting rules, one numbered line each.

escaping.jsp: quoting in template text and in scripting elements
<%@ page contentType="text/plain; charset=UTF-8" %>
1. ${books[0].title}
2. \${books[0].title}
3. \\${books[0].title}
4. \#{books[0].title}
5. ${'${'}books[0].title}
6. ${'}'}
7. <\% scriptlet %>
8. <%= "Joe said %\>" %>
9. <%= "Joe said \"hi\"" %>
10. Price: \$5 and $5 and #5
11. a backslash \ stays; so does \n
12. \\\${books[0].title}
13. \\\\${books[0].title}
Response of escaping.jsp
$
1. Dune$
2. ${books[0].title}$
3. \${books[0].title}$
4. #{books[0].title}$
5. ${books[0].title}$
6. }$
7. <% scriptlet %>$
8. Joe said %>$
9. Joe said "hi"$
10. Price: $5 and $5 and #5$
11. a backslash \ stays; so does \n$
12. \\${books[0].title}$
13. \\\${books[0].title}$

Reading the output line by line: line 1 is evaluated; line 2 shows that \$ produces a literal ${...} that is not evaluated; line 4 does the same for the deferred syntax; line 5 is the EL string-literal trick and produces the text ${books[0].title}, as the specification’s example ${'${'} predicts; line 6 shows ${'}'}; line 7 shows that <\% is the only way to write <% in template text; line 8 shows that %\> becomes %> inside a scriptlet string (without the backslash the string would end the scriptlet); line 9 is an ordinary Java string escape, because inside a scripting element the text is Java; line 10 shows that a lone $ or # is left alone; and line 11 shows that there is no escape for \n, so a backslash in template text is simply a backslash. The specification notes that the quoting rules "do not allow for quoting special characters such as \n", and that the way to produce one is a Java expression.

Lines 3, 12 and 13 reveal a behaviour that the specification does not describe for template text. On Tomcat 11.0.26 a pair of backslashes before ${ in template text collapses to a single backslash and the expression is then not evaluated (line 3 printed \${books[0].title}, not \Dune), and each additional pair behaves the same way. Inside an attribute value the rule of the specification applies and \\${...} evaluates the expression (see attributes.jsp below). To print a backslash followed by an evaluated value in template text, produce the backslash with EL, ${'\\'}${books[0].title}, or avoid it.

The #{ Sequence Is Reserved

Since JSP 2.1 the character sequence #{ is reserved for deferred EL expressions. In a JSP page a deferred expression makes no sense in template text, because only immediate evaluation can write text, so a #{ in template text is a translation error unless it is escaped as \#{ or the page opts in.

deferred-bare.jsp and deferred-literal.jsp: the reserved sequence and the opt-in
<%-- deferred-bare.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" %>
Total: #{books[0].title}

<%-- deferred-literal.jsp --%>
<%@ page contentType="text/plain; charset=UTF-8" deferredSyntaxAllowedAsLiteral="true" %>
Total: #{books[0].title}

The first page returned HTTP 500 with /deferred-bare.jsp (line: [2], column: [9]) #{...} is not allowed in template text, and the second returned HTTP 200 with the line Total: #{books[0].title} unchanged. The same switch exists for a group of pages as the deferred-syntax-allowed-as-literal element of jsp-property-group. Prefer \#{ for the few places where the text is meant literally; the opt-in hides real mistakes.

Quoting in Attribute Values

An attribute value is delimited by either single or double quotes, and a quote of the delimiter kind must be escaped with a backslash inside the value. Request-time scripting expressions inside a value are Java, so a Java string needs its own quotes, and each of the two levels of quoting multiplies the backslashes. The page below uses c:out with escapeXml="false" so that the characters reach the response exactly as the container decoded them.

attributes.jsp: quoting in attribute values
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
[<c:out value="plain 'single' inside double"/>]
[<c:out value='plain "double" inside single'/>]
[<c:out value="escaped \"double\" and \\ backslash" escapeXml="false"/>]
[<c:out value='escaped \'single\'' escapeXml="false"/>]
[<c:out value="entities &quot;double&quot; and &apos;single&apos;" escapeXml="false"/>]
[<c:out value="dollar \${not.el} hash \#{not.el}" escapeXml="false"/>]
[<c:out value='<%= "Joe said \\\"hi\\\"" %>' escapeXml="false"/>]
[<c:out value="<%= \"Joe said 'hello'\" %>" escapeXml="false"/>]
[<c:out value="<%= \"Joe said \\\"hello\\\"\" %>" escapeXml="false"/>]
[<c:out value="end expression %\>" escapeXml="false"/>]
[<c:out value="start <\% literal" escapeXml="false"/>]
[<c:out value="${shopName}"/>]
[<c:out value="${shopName}" escapeXml="false"/>]
[<c:out value="\\${books[0].title}" escapeXml="false"/>]
[<c:out value="\\\\${books[0].title}" escapeXml="false"/>]
[<c:out value="${'${'}x}" escapeXml="false"/>]
Response of attributes.jsp
[plain &#039;single&#039; inside double]$
[plain &#034;double&#034; inside single]$
[escaped "double" and \ backslash]$
[escaped 'single']$
[entities "double" and 'single']$
[dollar ${not.el} hash #{not.el}]$
[Joe said "hi"]$
[Joe said 'hello']$
[Joe said "hello"]$
[end expression %>]$
[start <% literal]$
[Bookshelf &#034;Corner&#034; &amp; Co.]$
[Bookshelf "Corner" & Co.]$
[\Dune]$
[\\Dune]$
[${x}]$

The first two lines also show c:out doing its job: it replaces <, >, &, ' and " by entity codes (' and " for the quotes), which is why the plain lines differ from the others. In the attribute value, the pair \\ becomes one backslash and ${...} is then evaluated, so \\${...} prints a backslash and the value, and \\\\${...} prints two (the last three lines), which is the rule in section 1.6 and the opposite of what template text did on line 3 above. The ninth line shows the Java and attribute levels together: a Java \" inside a double-quoted attribute is written \\\", because the attribute level consumes one backslash and the Java level the rest. The one illegal form worth memorising is the specification’s own: a request-time expression whose Java string uses the attribute’s own quote kind without escaping it.

err-attr.jsp: an unescaped double quote inside a double-quoted attribute
<%@ page contentType="text/plain" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<c:out value="<%= "hi!" %>"/>

Jasper stopped with /err-attr.jsp (line: [3], column: [17]) Attribute value [ "hi!" ] is quoted with ["] which must be escaped when used within the value. The fix is to switch the delimiter, value='<%= "hi!" %>', or to avoid the scripting expression altogether with value="hi!".

Switching EL Off: isELIgnored

Quoting is the answer for an occasional literal ${. When a whole page, or a whole set of legacy pages, contains dollar signs that must pass through, evaluation can be turned off instead. The default depends on the version of the deployment descriptor: a web application with a Servlet 2.4 or later web.xml evaluates ${...}; one with a Servlet 2.3 or earlier descriptor ignores EL "for backward compatibility". The default can be changed per group of pages with the el-ignored element of jsp-property-group (see Configuration and Property Groups) and per page with the isELIgnored attribute of the page directive; the page attribute wins unless it is unspecified.

Table 6. EL evaluation settings for JSP pages (Pages 4.0, Table JSP.3-1)
el-ignored (configuration) isELIgnored (page directive) Result

unspecified

unspecified

ignored for a Servlet 2.3 or earlier descriptor, evaluated otherwise

false

unspecified

evaluated

true

unspecified

ignored

any

false

evaluated

any

true

ignored

el-ignored.jsp: a page that passes the EL syntax through
<%@ page contentType="text/plain; charset=UTF-8" isELIgnored="true" %>
${books[0].title} and #{books[0].title} and \${x}

The response was ${books[0].title} and #{books[0].title} and \${x}$: with EL ignored, nothing is evaluated, the #{ sequence does not raise the translation error, and the backslash of \${x} is kept, because without EL there is nothing for it to quote. The setting also works the other way, and that is the migration trap discussed in "Where the Books Are Outdated". A second WAR with a Servlet 2.3 web.xml (a DOCTYPE of the 2.3 DTD) and a page that contains Sum: ${1 + 2} returned Sum: ${1 + 2} on Tomcat 11.0.26, whereas pages of the /g3syntax WAR, whose descriptor is Servlet 6.1, evaluate their expressions. Tag files do not take this default: they always evaluate expressions unless the tag directive says otherwise.

Escaping Is Not Output Encoding

The backslash rules decide what the translator sees; they do nothing for what the browser sees. ${shopName} in template text inserts the value as it is, so a name that contains <script> is delivered as markup. The two lines at the end of the attributes.jsp output above show both options side by side: c:out (or fn:escapeXml) produced Bookshelf "Corner" & Co., while the raw expression, which is what plain ${...} does in template text, produced Bookshelf "Corner" & Co.. Escaping on output, with c:out or fn:escapeXml(...), is the default rule for every value that is not known to be safe; Security covers the contexts (HTML body, attribute, JavaScript, URL, CSS) and the OWASP encoder library.

The secure form of the Bookshelf title cell
<%-- unsafe: the title is delivered as markup if it contains HTML --%>
<td>${b.title}</td>
<%-- safe in an HTML body and in a quoted attribute --%>
<td><c:out value="${b.title}"/></td>
<td title="${fn:escapeXml(b.title)}">...</td>

Whitespace

JSP follows the whitespace rule of XML: whitespace in template text is not significant but is preserved. Directives and JSP comments produce no output, but the line break after them is template text, which is why the page of the first example begins with two empty lines. In a response that is HTML the extra lines are harmless; in a plain-text, CSV or XML response they can be fatal (an XML declaration must be the first characters of a file).

whitespace.jsp and whitespace-trim.jsp: the same page, with and without trimming
<%@ page contentType="text/plain; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
Books: ${fn:length(books)}

<%-- whitespace-trim.jsp differs only in its first line: --%>
<%@ page contentType="text/plain; charset=UTF-8" trimDirectiveWhitespaces="true" %>
Response of whitespace.jsp
$
$
$
Books: 3$
Response of whitespace-trim.jsp (no line break after the last line)
$
Books: 3

The first page returned three empty lines (one per directive) and Books: 3 with its line break. The second returned one empty line and Books: 3 with no line break at all: template text that consists only of whitespace is removed, and the text \nBooks: (a line break and the word) is kept because it contains a non-whitespace character. That is the specification’s exact rule, and it explains why trimming leaves a leading line break and drops the trailing one. The configuration is the trim-directive-whitespaces element of jsp-property-group (false by default, no effect on JSP documents) or the trimDirectiveWhitespaces attribute of the page and tag directives. Tomcat adds a servlet parameter of its own, trimSpaces, with the values true, false, single and extended; single replaces such a run by one space and extended also removes leading and trailing whitespace of template text and collapses runs of whitespace and newlines to a single newline. The Tomcat documentation recommends single or extended to shrink responses in production. In a JSP document the rule is different and not configurable: text nodes that consist only of whitespace are dropped, except inside jsp:text, which is kept verbatim.

Template Text Between Scriptlets

The oldest JSP idiom, from the first generation of the technology, is to open a Java block in one scriptlet, write template text, and close the block in another. It is the reason why a scriptlet’s braces must balance across the page, and it is what a lot of existing code looks like, so the Bookshelf is shown in that form once and then in the current form.

blocks.jsp: Java blocks that surround template text (legacy, shown to be able to read it)
<%@ page contentType="text/html; charset=UTF-8" import="java.util.List,org.example.bookshelf.Book" %>
<ul>
<% for (Book b : (List<Book>) application.getAttribute("books")) { %>
  <% if (b.year() < 1900) { %>
  <li class="classic"><%= b.title() %></li>
  <% } else { %>
  <li><%= b.title() %></li>
  <% } %>
<% } %>
</ul>
el-vs-scriptlet.jsp: the same output with EL and the tag library
<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<ul>
<c:forEach var="b" items="${books}">
  <li class="${b.year lt 1900 ? 'classic' : ''}"><c:out value="${b.title}"/></li>
</c:forEach>
</ul>

The scriptlet page produced three items, with class="classic" only on Emma (1815); the EL page produced the same three items, with an empty class="" on the others (the conditional expression always writes the attribute, whereas the scriptlet version omits it). The braces in the first page span three scriptlets, and a missing <% } %> shows up as a compile error of the generated servlet rather than a translation error: err-scriptlet-split.jsp (an if block that is never closed) produced Syntax error, insert "}" to complete Block against a line of the generated .java file. Scripting Elements (Legacy) explains why this style is discouraged and how to disable it with scripting-invalid; the example is here only because the books use it heavily and because its translation rules are the same as for every other element.

Pitfalls

Table 7. Syntax and escaping pitfalls, with the symptom and the fix (all messages seen on Tomcat 11.0.26)
Pitfall Symptom Fix

#{...} in template text

HTTP 500, #{...} is not allowed in template text

write \#{, or set deferredSyntaxAllowedAsLiteral for a page that really needs it

Missing %> or a closing marker in another file

Unterminated [<%] tag, naming the fragment when the open element is in an include

close every element in the file that opened it

JSP comment never closed

Unterminated [<%--] tag

add --%>

Nested JSP comment

output contains the tail of the inner comment, such as B --%>C

JSP comments do not nest; remove the inner one

Unescaped quote in an attribute

Attribute value [ "hi!" ] is quoted with ["] which must be escaped when used within the value

use the other delimiter, \", \' or an entity

Wrong case in an action name

No tag [Out] defined in tag library imported with prefix [c]

action and attribute names are case-sensitive

Undeclared prefix

no error: <cc:forEach> is delivered to the browser as template text, and EL in its attributes is still evaluated

declare the prefix with a taglib directive and check the generated HTML

Same attribute set twice

The attribute [value] specified in the standard or custom action also appears as the value of the name attribute in the enclosed jsp:attribute

give the value once

Unbalanced Java block

Syntax error, insert "}" to complete Block in the generated source

balance braces across scriptlets, or use tags

<% %> in a .jspx file

The content of elements must consist of well-formed character data or markup.

use the jsp:scriptlet form, or better no scripting

Data in an HTML comment

the comment, with the evaluated data, is in "view source"

use <%-- --%>

${...} in template text assumed to be escaped

markup from the value is delivered as markup

wrap in c:out or fn:escapeXml

\\${...} in template text

a backslash and the unevaluated expression (Tomcat 11.0.26), unlike in an attribute value

avoid; generate the backslash with ${'\\'}

Old web.xml (Servlet 2.3 DTD)

${...} printed literally, no error

declare Servlet 6.1 (version="6.1") or set el-ignored to false

Fixing the error and reloading

a later request can report ClassNotFoundException for the page class instead of the original message (seen on the second request of a page that failed to translate)

read the first message and the Tomcat log; the page is retried when the file changes

What Changed Recently

The rules on this page are old and have been stable. The change log in Appendix C of the Pages 4.0 specification lists no change to the core syntax, the comment forms or the quoting rules between JSP 3.1 and 4.0; the changes are the removals around them. The history that matters for syntax is:

  • JSP 2.0 reserved ${...} for EL in template text and attribute values, and introduced \$ as the way to quote it.

  • JSP 2.1 reserved #{...}, which is why #{ in template text is a translation error unless allowed.

  • A web application with a Servlet 2.4 or later descriptor evaluates EL by default; older descriptors ignore it. This is the default that Tomcat 11 still applies to a Servlet 2.3 web.xml.

  • JSP 3.0 (Jakarta EE 9) moved the API from javax.servlet.jsp to jakarta.servlet.jsp; the syntax did not change. Tag library identifiers moved too: jakarta.tags.core and friends are the JSTL 3.0 URIs, and the old http://java.sun.com/jsp/jstl/core identifiers are still accepted as identifiers (they are not links).

  • JSP 3.1 (Jakarta EE 10) added the errorOnELNotFound page attribute and deprecated isThreadSafe and jsp:plugin.

  • JSP 4.0 (Jakarta EE 11) requires Java 17, removes isThreadSafe and jsp:plugin, and removes the deprecated jakarta.servlet.jsp.el classes, BodyTag.EVAL_BODY_TAG and JspException.getRootCause. None of these touches the syntax described on this page.

Where the Books Are Outdated

The books consulted for this section target JSP 1.2 and JSTL 1.0 (and Servlet 2.3); this page uses them only for the list of topics. What changed since:

Table 8. Where the JSP 1.2 and JSTL 1.0 era differs from the current rules
Area Then and now

EL in template text

JSP 1.2 had no expression language; ${...} in a page was plain text, and JSTL 1.0 evaluated EL only inside the attributes of its own tags. Since JSP 2.0 the container evaluates EL in template text and in attributes of all tags. A 1.2-era page that contained a literal ${...} changes its output the moment its web application is upgraded to a Servlet 2.4 or later descriptor. Quote it, or set el-ignored

#{...}

does not exist in the books; it is reserved and an error in template text

jsp:root

mandatory in JSP 1.2 documents; the specification now says most JSP 4.0 documents will not use it

XML spellings of scripting elements

presented in the books as an alternative form; still valid, and the only form of scripting element allowed in a JSP document

Dynamic HTML comments

<!-- <%= expression %> --> for output with a time stamp or a debug value; still valid, but for diagnostics prefer server logs, since the comment is public

Typographic quotes in listings

some code samples print curly quotes, which are not valid attribute delimiters if the code is copied; the quoting rules above need straight " and '

Scriptlets as the normal way to write loops and conditions

replaced by JSTL and EL; the Java block form is legacy

web.xml DTD and <taglib> mapping

Servlet 2.3 DTD; replaced by the Servlet 6.1 schema, jsp-config, and tag library discovery by URI

javax.servlet.jsp

jakarta.servlet.jsp since Pages 3.0