My first open source contribution (1) — An invisible space before an email address

networknt/json-schema-validator #1164 → PR #1267

For my first open source contribution, I fixed an email validation bug in networknt/json-schema-validator, a Java library that checks JSON documents against JSON Schema. Issue #1164 reported that an email address could pass validation even with a space at the beginning.

The result depended on which space it was. An ordinary space typed with the space bar was rejected, but a non-breaking space, or NBSP, passed. NBSP is the character represented by   in HTML; it keeps a line from breaking at that position. An ordinary space is U+0020, while NBSP is U+00A0. Both inputs could look like " name@email.com" on screen, even though they contained different characters.

The issue had been open since April 2025, more than a year earlier. The description was short, but it included an input and a link to the suspected regular expression. That gave me somewhere to start.

Reproducing the difference

I wanted to compare an ordinary space and an NBSP prepended to the same email address. At first, though, even invalid email addresses produced no errors. Before I could investigate NBSP, I had to check whether email format validation was running at all.

Adding {"format": "email"} to the schema was not enough. JSON Schema's format describes a string's format; enforcing it is a separate choice. The draft 2019-09 release notes explain the change to treating formats as annotations by default. In this library, I needed formatAssertionsEnabled(true) to enable email format checks.

Enable format assertions

schema.validate(input, InputFormat.JSON,
    ctx -> ctx.executionConfig(cfg -> cfg.formatAssertionsEnabled(true)));

With that option enabled, the ordinary space was rejected and NBSP passed. I followed the validation code to see where the two characters were treated differently.

What the regular expression considers whitespace

EmailFormat delegates to IPv6AwareEmailValidator, which extends a copy of Apache Commons Validator's EmailValidator. The parent class uses a regular expression to check which characters can appear before the @.

EmailValidator.java

private static final String SPECIAL_CHARS = "\\p{Cntrl}\\(\\)<>@,;:'\\\\\\\"\\.\\[\\]";
private static final String VALID_CHARS = "(\\\\.)|[^\\s" + SPECIAL_CHARS + "]";

Here, [^\s...] allows characters other than whitespace and the listed special characters. But in Java, \s matches only six ASCII whitespace characters by default: space, tab, LF, VT, FF, and CR. NBSP is outside that set and passed the regular expression check.

Change the expression or add a check?

I could either expand the whitespace recognized by the regular expression or check for it before calling the validator. To decide where the change belonged, I looked through earlier PRs. PR #741 had removed local changes to the Apache Commons Validator code and moved it into a separate package.

By contrast, the time fix in PR #1188 and the uri and iri fixes in PR #983 added checks in the classes responsible for those formats. I followed that approach: leave the copied EmailValidator alone and check whitespace in EmailFormat first.

My first thought was to reject any address containing whitespace. But ASCII spaces are allowed when the part before @ is enclosed in quotes. The official JSON-Schema-Test-Suite treats this address as valid:

Quoted local part

"joe bloggs"@example.com

Rejecting all whitespace would also reject this address. I decided to leave ASCII characters to the existing validator and reject only non-ASCII whitespace beforehand. I added the check to EmailFormat.matches(), before the call to the validator.

Before · EmailFormat.java

@Override
public boolean matches(
        ExecutionContext executionContext,
        String value) {
    return this.emailValidator.isValid(value);
}

After · EmailFormat.java

@Override
public boolean matches(
        ExecutionContext executionContext,
        String value) {
    if (containsNonAsciiWhitespace(value)) {
        return false;
    }
    return this.emailValidator.isValid(value);
}

For the whitespace check, I looked at Character.isWhitespace(). The name suggested it would cover what I needed, but Character.isWhitespace(0x00A0) returned false.

0x00A0 is Java's hexadecimal integer literal for U+00A0, the code point for NBSP.

isWhitespace() alone would miss the very character that caused the bug. I had to use Character.isSpaceChar() as well. U+2007 and U+202F are also spaces excluded by isWhitespace(). The check rejects a character if either method identifies it as whitespace, with ch > 0x7F keeping ASCII characters out of this check.

EmailFormat.containsNonAsciiWhitespace

private static boolean containsNonAsciiWhitespace(String value) {
    if (value == null) {
        return false;
    }
    return value.codePoints()
            .anyMatch(ch -> ch > 0x7F
                    && (Character.isWhitespace(ch) || Character.isSpaceChar(ch)));
}

Testing an invisible character

I added tests to EmailFormatTest for a valid email address and for one with an NBSP at the beginning. Putting a literal NBSP in the source would make it hard to distinguish from an ordinary space. If someone accidentally replaced it with an ordinary space, the validator would still reject the address and the test would still pass. I built the character from its code point so the source made clear what was being tested.

NBSP test fixture

/** U+00A0 NON-BREAKING SPACE, built from its code point so no invisible character sits in the source. */
private static final String NBSP = new String(Character.toChars(0x00A0));

I prepended that string to an email address and checked that validation returned an error.

EmailFormatTest.emailWithLeadingNbspShouldFail

@Test
void emailWithLeadingNbspShouldFail() {
    List<Error> messages = validateEmail(NBSP + "name@email.com");
    assertFalse(messages.isEmpty(), "email with a leading non-breaking space (U+00A0) should be invalid");
}

I checked idn-email and found the same problem. I kept this PR limited to email, the format covered by issue #1164, and noted the remaining idn-email problem in the PR description. I opened PR #1267 with the results of the full test suite, including the official JSON-Schema-Test-Suite's email cases. It was my first PR, and I was a little nervous waiting for the review.