EngineeringEmad Khair14 min read

A Unicode Escape Can Turn a Java Comment Into Executable Code

What happens when an SDK generator copies documentation from an OpenAPI specification into source code without escaping it?

A Unicode Escape Can Turn a Java Comment Into Executable Code

The Java program was supposed to do nothing.

It contained a small class with a main() method and a single line of what appeared to be commented-out text. I compiled it with javac, ran it, and got an unexpected result.

It printed Hello.

Here's the source:

java
class Example {
    public static void main(String[] args) {
        // Description: \u000a System.out.println("Hello");
    }
}

Look at the third line. The System.out.println() statement appears after //, which should make it part of the comment.

Java disagrees.

The sequence \u000a represents a newline. According to the Java Language Specification, Unicode escapes are processed before the compiler identifies comments, strings, and other tokens.

By the time Java reaches the comment, the source effectively looks like this:

java
class Example {
    public static void main(String[] args) {
        // Description:
        System.out.println("Hello");
    }
}

The compiler sees a perfectly valid program and executes the statement.

I reproduced this with OpenJDK 21. The behavior is part of Java's language specification and has been for a long time.

What makes it interesting is where that original comment might have come from.

A developer probably wouldn't write this intentionally. A code generator, however, might produce something very similar while copying a description from an API specification.

And the generator has to understand the Java compiler well enough to prevent it.

The Trip From OpenAPI to Source Code

An OpenAPI document describes an API using structured data.

It contains endpoint paths, request parameters, response schemas, authentication requirements, and the other information a client needs to communicate with a server.

It also contains a lot of ordinary text.

Consider this example:

yaml
paths:
  /users:
    get:
      summary: List users
      description: Returns the users available to the caller.

An SDK generator reads this document and produces the corresponding client method.

In Java, that might look something like:

java
/**
 * Returns the users available to the caller.
 */
public List<User> listUsers() {
    // ...
}

The description has become documentation in the generated SDK. A developer can hover over the method in an IDE and read the explanation that came from the API specification.

It's a useful feature, and the implementation seems straightforward enough.

Read the description. Place it inside a comment. Write the source file.

Except that the description belongs to a different language.

In the OpenAPI document, it's a string. The YAML parser treats it as data. It has no reason to care whether the string contains Java comment delimiters, Ruby interpolation syntax, or PHP closing tags.

Once the generator writes that same string into a source file, those characters acquire new meaning.

The language compiler doesn't know they came from an OpenAPI document. It sees characters that form part of a program.

That's the boundary a generator has to protect.

OpenAPIdocumentdescription: astring in YAMLSDK generatorstructured databecomes sourcecodeContext-awareencoderescaped for aJava commentDirect textinsertionstring copied asisDescriptionstays acommentJava sourcestructureunchangedProgramstructurechangesa Java sequenceleaves thecommentOpenAPI documentdescription: a string in YAMLSDK generatorstructured data becomes source codeContext-awareencoderescaped for a JavacommentDirect textinsertionstring copied as isDescriptionstays a commentJava sourcestructure unchangedProgramstructurechangesa Java sequenceleaves the comment
Where a description crosses from structured data into source code

The Java example shows one way that boundary can fail. Other languages have their own ways of interpreting text that looks harmless in a specification.

I started looking at a few of them.

PHP Has Its Own Idea of Where a Comment Ends

PHP seemed like a natural next place to look because it has an unusual relationship with comments and source-file boundaries.

A PHP file can move between PHP code and content outside PHP tags. That's useful for templates and server-rendered pages, but it introduces behavior that matters when generating source.

Consider a line comment:

php
<?php
// Description: Returns the current user.

The usual expectation is that everything after // remains commented out until the newline.

PHP has an additional rule.

The closing tag ?> ends the current PHP block, even when it appears inside a single-line comment.

The PHP manual explicitly documents this behavior.

I reproduced it using PHP 8.5. A closing tag inside a line comment switched the interpreter out of PHP mode. When a subsequent opening tag appeared, PHP resumed executing code.

For someone writing PHP by hand, the behavior is understandable. PHP's opening and closing tags are fundamental parts of its syntax.

For an SDK generator, it creates another place where the surrounding template doesn't necessarily contain the input as expected.

Suppose the generator uses a PHP line comment to document a method. The description came from an OpenAPI file, and the template author assumes the comment prefix will make its contents harmless.

That assumption holds only if the generator accounts for PHP's tag handling.

Java's Unicode processing and PHP's tag transitions are different mechanisms, but they create the same problem for generated source: characters supplied as documentation can influence how the language parses the file.

The remedy has to be applied before the source reaches the compiler or interpreter.

In PHP, that means handling tag delimiters when writing comments. In Java, it means accounting for Unicode-escape processing before comments are recognized.

And comments are only part of the problem.

Ruby Makes the Problem Easier to See

Documentation fields aren't the only free-text values in OpenAPI.

Specifications also contain examples, defaults, enum values, and other strings that generators frequently copy into source-code literals.

Ruby provides a useful demonstration of why those values need equal care.

Suppose a specification contains this example:

yaml
example: 'Result: #{1 + 1}'

The intended value is the literal string Result: #{1 + 1}.

Now imagine a Ruby generator constructing the following constant:

ruby
EXAMPLE = "Result: #{1 + 1}"

Ruby evaluates expressions inside #{...} when they appear in double-quoted strings.

The value of EXAMPLE is therefore:

text
Result: 2

The generated code compiles. The program runs successfully. The value is wrong.

The Ruby documentation describes interpolation as part of the language's string literal syntax.

I reproduced this behavior as well. Ruby interpolation also permits expressions with side effects, which makes the consequences more significant than an incorrectly generated example.

A generator that assembles Ruby literals by surrounding arbitrary input with double quotes risks allowing that input to become an expression.

The fix is to use a literal writer that preserves the original value.

Ruby offers several string representations. Single-quoted strings, for example, don't perform #{...} interpolation.

But a generator still has to escape characters that are special to a single-quoted literal. Switching quotation styles doesn't remove the need to handle delimiters.

The important requirement is that the resulting program contains the exact value supplied by the specification.

In the example above, evaluating EXAMPLE should return the characters Result: #{1 + 1}, including the braces and expression text.

Anything else means the generator changed the data.

Compilation success doesn't guarantee correctness

The Ruby case is particularly useful because it illustrates a failure that can survive compilation.

Many generator tests stop after checking that the generated SDK builds.

That's a reasonable first check, but it wouldn't catch a string whose interpolation changed the value.

The same issue applies to defaults, enum members, and other generated literals.

For those fields, correctness means that the value represented by the generated source is identical to the value provided by the specification.

A compiler can tell us whether a program is syntactically valid.

It can't tell us whether a code generator preserved the meaning of its input.

There Are More Places for Text to Escape

After examining Java, PHP, and Ruby, the pattern becomes familiar.

Every programming language has syntactic boundaries. A generator has to know which boundaries surround the text it's writing.

Some are obvious, such as quotation marks and block-comment terminators.

Others involve language-specific behavior that can be easy to forget when working on a generator supporting several languages.

Here are a few examples:

Language Source position Behavior
Java Line comment Eligible Unicode escapes are translated before tokenization
PHP Line comment ?> can end the PHP block
Ruby Double-quoted string #{...} evaluates an expression
Ruby Block comment =end at the beginning of a line closes an =begin comment
Kotlin String literal ${...} evaluates a template expression
Python Triple-quoted docstring Matching triple quotes terminate the string
Go Raw string literal A backtick terminates the literal
Java JavaDoc comment Malformed HTML can fail documentation validation
Rust Documentation comment Rust code fences can become documentation tests

I also reproduced several of these additional behaviors, including Kotlin interpolation, Go raw-string termination, Python docstring diagnostics, and JavaDoc validation failures.

Go provides a particularly simple example.

Its language specification defines raw string literals using backticks.

If a generator places an arbitrary description or example into a backtick-delimited literal, any backtick in the input has to be accounted for. Otherwise, it can terminate the literal.

The result may be a compiler error.

A developer using an SDK generator might encounter this as an apparently broken release: a specification is accepted, source files are generated, and then the build fails because a description or example contains characters the generator didn't handle.

Python has a similar issue with triple-quoted docstrings.

If a generator places arbitrary documentation between """ delimiters, matching triple quotes in the text can end the string earlier than expected.

Backslashes introduce another concern. In Python 3.13, a non-raw docstring containing an unrecognized escape such as \d produces a SyntaxWarning, as described in the Python lexical analysis documentation.

JavaDoc introduces problems during documentation generation.

Its documentation tool validates HTML within comments. An unclosed HTML tag can cause documentation generation to fail, even when the Java source otherwise compiles.

These failures often appear far from the original specification field that caused them.

A build log points to a generated source file. The developer opens the file and finds invalid syntax somewhere in a comment or string literal.

The underlying cause is the earlier transformation from specification text to source code.

Rust Documentation Has an Additional Responsibility

Rust adds another interesting case because its documentation system can compile and execute examples.

Rustdoc supports Markdown inside documentation comments.

A fenced Rust code block can become a documentation test.

For example:

rust
/// Adds two numbers.
///
/// ```rust
/// assert_eq!(2 + 2, 4);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

During documentation testing, rustdoc can extract the code block, compile it, and execute it.

This is a valuable feature. It allows a project to keep examples synchronized with the implementation.

It also means a generator writing Rust documentation needs to decide how code fences should behave.

OpenAPI descriptions frequently contain Markdown. Some contain code examples, and those examples may be intended only for display.

If a generator copies a description into a Rust documentation comment without accounting for rustdoc's code-block rules, a displayed example may become a test.

The rustdoc guide documents the available controls.

A text fence is appropriate for code that should be displayed as ordinary text. An ignore fence prevents a snippet from being compiled as a documentation test. The no_run option still allows compilation but skips execution.

Those distinctions matter when a generator processes Markdown it doesn't control.

A generated documentation comment should produce exactly the kind of documentation the template author intended.

For Rust, that includes deciding whether a code example is executable.

OpenAPIMarkdowndescriptioncontains afenced codeblockRust SDKgeneratorchooses thefence'ssemantics///documentationcommentrustdocdocumentationtestuncontrolledcode fence:compiled andexecutedRendereddocumentationexplicit text orignore fence:displayed onlyOpenAPI Markdown descriptioncontains a fenced code blockRust SDK generatorchooses the fence's semantics/// documentation commentrustdocdocumentationtestuncontrolled codefence: compiled andexecutedRendereddocumentationexplicit text orignore fence:displayed only
The generator decides whether a code fence from the spec runs

The Fix Belongs in the Generator

Once you start thinking about all these language rules, a generic sanitization function becomes tempting.

Take a description, remove a few dangerous characters, and reuse the result everywhere.

That approach breaks down quickly.

The rules for a Java line comment differ from those for a JavaDoc block comment.

Ruby's double-quoted strings behave differently from its single-quoted strings.

PHP comments have tag-related behavior. Go raw strings use backticks. Rust documentation supports executable code fences.

Even within one language, a value that's safe in a comment may be unsafe in a string literal.

The generator needs to understand the precise source position where each value will appear.

I would organize source generation around language-specific emitters.

For example:

text
writeJavaDocText(description)
writeJavaStringLiteral(value)
writePhpComment(description)
writeRubyStringLiteral(value)
writeGoStringLiteral(value)
writeRustDocumentation(description)

These are conceptual interfaces, but the separation is useful.

Each emitter receives a value and produces valid source text for one particular context.

The rest of the generator doesn't need to know whether Java processes Unicode escapes before comments or whether Ruby supports interpolation inside double quotes.

Those details live in the component responsible for producing the final source representation.

This also keeps the code-generation pipeline easier to reason about.

The OpenAPI parser can preserve the original values. The generator can build a structured representation of the SDK. The language-specific writer can then encode those values when emitting source.

The longer values remain structured data, the fewer opportunities there are for accidental interpretation as source syntax.

Preserve the original value

Escaping should also preserve meaning.

Suppose an API specification contains a string representing a Windows path:

yaml
example: 'C:\temp\logs'

The generated SDK should preserve those characters exactly.

Removing backslashes would make the value safe from some syntax problems while also making the generated output incorrect.

The same applies to quotation marks, Unicode characters, regular expressions, and Markdown.

The source representation may need additional escaping characters, or an entirely different literal form.

But evaluating the generated value should recover the original input.

This suggests a useful invariant for generator tests:

Decode the generated value and compare it with the original specification value. They should be identical.

For documentation, the equivalent requirement is that the description remains inside the intended comment or documentation structure without altering the surrounding program.

Test With the Compiler That Will Read the Output

I began this investigation with a Java file that looked harmless and printed Hello.

The interesting behavior appeared only when the source went through javac.

That's a good reason to test SDK generators against the real tools that consume their output.

For every language a generator supports, I'd include a set of specifications containing unusual but valid field values.

Descriptions with comment terminators.

Examples containing quotation marks and interpolation markers.

Defaults containing backslashes.

Markdown with code fences.

Text containing Unicode escape sequences.

Then I'd generate the SDK and run it through the normal development workflow for that language.

Compile the source. Build the documentation. Run the tests. Check that generated values are preserved exactly.

For Rust, verify that documentation examples are treated as intended.

For Java, include cases that account for Unicode translation before tokenization.

For Ruby and Kotlin, make sure text originating from the specification isn't evaluated as an unintended interpolation expression.

For PHP, verify that generated comments cannot change parser mode.

There's a useful difference between testing the generator's string-replacement logic and testing the program it produces.

A unit test might confirm that a description was inserted into a template.

The compiler can reveal what that insertion actually means.

Edge-caseOpenAPI specdescriptions,enums,defaults,examplesSDKgeneratorJava and PHPSDKsRuby and GoSDKsPython andRust SDKsEachlanguage'sown toolscompiler,interpreter,docsSourceparsescorrectlyNounintendedexecutioncode ordocumentationtestsValuesround-tripunchangedcompared withthe originalspecEdge-case OpenAPI specdescriptions, enums, defaults, examplesSDK generatorJava andPHP SDKsRuby andGo SDKsPythonand RustSDKsEach language's own toolscompiler, interpreter, docsSourceparsescorrectlyNounintendedexecutioncode ordocumentationtestsValuesround-tripunchangedcomparedwith theoriginalspec
Correctness is measured against the input, not only against compilation

Back to the Java Comment

That small Java program is still my favorite example from this investigation.

The source looked ordinary enough:

java
// Description: \u000a System.out.println("Hello");

A developer looking at the line might expect the statement to be ignored.

Java's lexical rules produce a different interpretation.

Once you know that Unicode translation happens before comment recognition, the result makes sense.

The same reasoning explains the PHP and Ruby examples. Their parsers follow well-defined rules, and the results are predictable when you know those rules.

The difficulty for an SDK generator is that it has to know all of them.

It may accept an OpenAPI document containing perfectly valid strings and then write those strings into six or ten different programming languages. Each target has its own grammar, string forms, comment delimiters, and documentation conventions.

A single description can travel a long way from the YAML file where it began.

By the time a developer compiles the resulting SDK, the text has crossed several boundaries. The generator is the component responsible for making those crossings safe.

The approach I'd trust is to retain the original specification values, use context-aware emitters for each destination, and verify the generated artifacts with the actual language toolchains.

That gives the generator a clear responsibility: produce source code whose structure comes from its templates and whose data comes from the specification.

A description should be able to contain unusual characters without changing the program around it.

The compiler will always interpret the characters it receives according to its language rules.

The generator's job is to make sure those rules never give an OpenAPI description more authority than it was meant to have.

Emad Khair