Articles
cf-JEmoji: a CFML Emoji Wrapper for the Post-emoji-java Era
Seven years ago I released cf-emoji-java and shared it on my dev.to blog.
"A ColdFusion application that we developed a couple years ago worked with Twilio to log incoming text messages. The UTF-8 message payloads were saved in a MSSQL database using the NVARCHAR datatype and could be displayed on webpages without any issue. When importing a CSV file into a third-party Windows program, a random error would cause the import to abort whenever it encountered a high ASCII character. We didn't want to strip out the data, but we also didn't want to convert emojis to HTML entities or decimal values as they would be somewhat meaningless outside of an HTML environment."
cf-emoji-java is a small CFC that wrapped Vincent Durmont's emoji-java Java library so CFML apps could ask easy questions about emojis; is this string an emoji, what's its alias, swap unicode for :thumbsup:, that sort of thing. The CFC has been quietly running in production for clients ever since.
The problem: emoji-java was last updated in 2018. The Unicode Consortium kept shipping new emojis. Skin-tone modifiers, gendered family clusters, ZWJ sequences, the whole pile... and none of them recognized by a library frozen at Unicode 11. I left a comment on the project's issue #204 thread back in 2023 asking about updates; the maintainer eventually archived the repo and pointed everyone at JEmoji, an actively-maintained replacement by Dominic Fellbaum that tracks current Unicode releases.
So I wrote a new wrapper. This is the story of building cf-JEmoji, and a few of the surprises I hit along the way that other CFML developers integrating modern Java libraries are likely to bump into.
What the wrapper looks like
cf-JEmoji is a single JEmoji.cfc file with about 30 public methods grouped into Booleans, Lookup, Bulk Retrieval, Transformation, Extraction, and Language. Same shape as cf-emoji-java where the API overlapped, plus the new things JEmoji can do that emoji-java couldn't:
jemoji = new JEmoji();
jemoji.isEmoji("👍"); // true
jemoji.containsEmoji("Hello 👍 World"); // true
jemoji.parseToAliases("Hi 👍"); // "Hi :thumbsup:"
jemoji.parseToHtmlDecimal("Hi 👍"); // "Hi 👍"
jemoji.removeAllEmojis("Hello 👍"); // "Hello "
jemoji.extractEmojis("a 👍 b 😀"); // [{emoji,description,...}, {...}]
// Things emoji-java never had:
jemoji.getDescription("👍", "de"); // "Daumen hoch"
jemoji.getDescription("👍", "fr"); // "pouce vers le haut"
jemoji.getAllEmojisByGroup("FOOD_AND_DRINK");
jemoji.getAllEmojisGrouped();
There's a returnFullDetail flag (boolean, defaults to false) for methods that return emoji structs - minimal by default to keep payloads small, full when you actually want every field. Set it on the wrapper for a default, override per call when needed.
Surprise #1: JEmoji 2.0.0 is NOT Java 8
JEmoji's GitHub README lists Java 8 in the badges. Several AI assistants will confidently tell you JEmoji has no external dependencies and runs on Java 8 - which was true for the 1.x line. The published 2.0.0 JAR tells a different story.
Crack open jemoji-2.0.0.jar, look at the first eight bytes of any .class file, and you'll see cafe babe 0000 003d. That 0x003d is class file major version 61 - meaning Java 17. The JAR also ships a module-info.class (JPMS module), which by definition can only exist on Java 9+. There's no META-INF/versions/N/ multi-release fallback either. So Adobe ColdFusion 2016 / Java 11, which is what I started prototyping against, threw UnsupportedClassVersionError: class file version 61.0 on the very first createObject("java", ...) call.
I switched the project to Adobe ColdFusion 2025 + JDK 24 (the only Adobe-certified pairing on this machine that supports JEmoji's bytecode). Lucee 6 and BoxLang 1.x both run on Java 17+ and would work too.
Surprise #2: Jackson 3.x is required by the BASE library
The README says nothing about external dependencies. The published Maven POM says otherwise:
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.1.2</version>
<scope>runtime</scope>
</dependency>
This is Jackson 3 - the new package namespace tools.jackson.*, not the long-running com.fasterxml.jackson.* of Jackson 2. JEmoji uses it to parse the bundled emoji data file on first call. Four classes inside jemoji-2.0.0.jar import tools.jackson.* directly:
net/fellbaum/jemoji/internal/EmojiData$InitHelper.classnet/fellbaum/jemoji/internal/EmojiData$InitHelper$1.classnet/fellbaum/jemoji/internal/ResourceFilesProvider.classnet/fellbaum/jemoji/internal/ResourceFilesProvider$1.class
So the very first EmojiManager.getEmoji(...) call inside the CFC's init() blew up with NoClassDefFoundError: tools/jackson/core/type/TypeReference until I added three Jackson 3.x JARs alongside the JEmoji JARs. (Jackson 3 deliberately keeps the annotations artifact at the legacy com.fasterxml.jackson.core group ID per the project's compatibility plan, which is why one of the three JARs is jackson-annotations-2.21.jar rather than 3.x. That's not a typo on my README.)
Surprise #3: Adobe CF's per-JAR classloader breaks Java SPI
This one cost me an hour. I added each JAR to Application.cfc.this.javaSettings.loadPaths as a separate file path, restarted, and verified that EmojiManager and EmojiLanguage both loaded cleanly. isLanguageSupported() returned true. Then I called getDescription("👍", "de") and got:
Trying to access a property for language "de" but the jemoji-language module
is missing.
Which is impossible, because the language JAR was right there.
Two layers had to be unstuck:
Layer one - per-JAR loadPaths split into separate URLClassLoaders. Adobe CF / CommandBox's dynamic javaSettings.loadPaths puts each individual JAR file into its own JavaDynamicClassLoader. JEmoji's language module uses Java's SPI mechanism (ServiceLoader.load), which requires the SPI registration in jemoji-languages-2.0.0.jar's META-INF/services/... to be visible from the same classloader as the consumer (the base jemoji-2.0.0.jar). Per-JAR paths broke that visibility. Fix: point loadPaths at the directory containing all five JARs instead of listing each JAR. Adobe CF loads a directory entry's contents into a single classloader, restoring SPI visibility.
Layer two - thread context classloader is the engine's bootstrap CL. Even with all JARs in one CL, getDescription still threw. Diagnostic logging confirmed:
EmojiManager CL: JavaDynamicClassLoader@...
SPI service file visible from EmojiManager CL: YES
ServiceLoader thread context CL: BootstrapClassLoader@... <-- different CL
SPI service file visible from thread context CL: NO
ServiceLoader.load(provIface).iterator() count: 0
ServiceLoader.load(Class) (one-arg form) reads the thread context classloader, NOT the calling class's CL. On Adobe CF the thread context CL is the engine's BootstrapClassLoader, which has no idea our JARs exist. The fix is a small private helper that swaps the thread context CL to JEmoji's CL while running language calls, then restores it in a finally block:
private any function _withJEmojiThreadCL(required any callable) {
var thread = createObject("java", "java.lang.Thread").currentThread();
var prev = thread.getContextClassLoader();
thread.setContextClassLoader(variables.EmojiManager.getClass().getClassLoader());
try {
return arguments.callable();
} finally {
thread.setContextClassLoader(prev);
}
}
getDescription, getKeywords, and the init() probe all wrap their bodies in this. After both fixes, getDescription("👍", "de") cleanly returned "Daumen hoch" and the language test suite went green.
If you're integrating any modern Java library that uses ServiceLoader.load(Class) - JDBC drivers, Jackson modules, anything with META-INF/services/... - this CL-swap pattern is going to come up. It's worth knowing.
Surprise #4: A few real upstream JEmoji bugs
Tests caught three things wrong with JEmoji 2.0.0 itself, all worked around in the wrapper:
-
Emoji.getKeywords(EmojiLanguage)always throwsClassCastException. The keyword data file shape doesn't match what JEmoji's deserializer expects. Reproduced across every emoji x language combination I tried. Wrapper catches the exception and returns an empty array; an upstream fix is the only real solution. -
EmojiManager.getByHtmlHexadecimalrequires uppercase&#X...;even thoughEmoji.getHtmlHexadecimalCode()itself emits lowercase&#x...;. Round-tripping the API's own output back through the lookup fails. Wrapper normalizes the prefix to uppercase before delegating. -
EmojiManager.getByAliasreturnsOptional<List<Emoji>>, notOptional<Emoji>. An alias can map to multiple emojis. Wrapper takes the first match for cf-emoji-java parity; if anyone needs the full list I'll add agetAllByAlias()sibling.
Tests and demo
The wrapper ships a plain-CFML test harness - no TestBox dependency. Two suites:
tests/test_JEmoji.cfm- 53 assertions covering booleans, lookup, bulk, transformation, extraction (returns HTTP 500 on any failure so CI / curl can detect it)tests/test_JEmoji_languages.cfm- 8 assertions covering the language module, loudly fails at the top if the languages JAR isn't loaded
Plus demo.cfm, an interactive playground that exercises every public method against an editable input string. JSON dumps are pretty-printed and prism-highlighted; results are shown as monospace chips so they're visually distinct from labels.
Each page prints a small banner under its title showing the running engine, Java version, and current ISO datetime - handy for confirming exactly which stack you're testing against.
Get it
- Repo: https://github.com/JamoCA/cf-JEmoji
- Upstream library: https://github.com/felldo/JEmoji
- License: Apache 2.0
JARs from Maven Central (use the latest released version of each):
net.fellbaum:jemoji- base librarynet.fellbaum:jemoji-languages- optional, enables the language moduletools.jackson.core:jackson-coreandjackson-databind- Jackson 3.x runtimecom.fasterxml.jackson.core:jackson-annotations- kept at the legacy group ID per Jackson 3's compatibility plan
The README in the repo lists the specific versions cf-JEmoji was developed and tested against, but the Maven artifact pages always show the current release - take the latest unless something obvious breaks.
Takeaways
If you're carrying a CFML wrapper for a Java library forward, the upstream's README is a starting point, not an authority. Verify bytecode version against the actual published JAR, read the POM for declared runtime dependencies, and assume any modern library you pull in will use SPI somewhere - which means you need to think about how your engine's classloader topology interacts with ServiceLoader.load. Those three checks would have saved me a couple of hours on this rebuild.
The good news: once those issues are sorted, JEmoji is a strong replacement for the original emoji-java. It tracks Unicode versions, supports descriptions and keywords in 168 languages, and exposes structured data (groups, subgroups, qualification status, Fitzpatrick / hair / variation flags) that the older library never had. cf-JEmoji passes that surface through to CFML with the same conventions as its predecessor, so migrating from cf-emoji-java is mostly a search-and-replace job.