<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: christoph-sens</title>
    <description>The latest articles on DEV Community by christoph-sens (@christophsens).</description>
    <link>https://dev.to/christophsens</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4144605%2F41c75140-9001-42b5-ba91-5c0ee818a68d.png</url>
      <title>DEV Community: christoph-sens</title>
      <link>https://dev.to/christophsens</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christophsens"/>
    <language>en</language>
    <item>
      <title>Treetrail: Why I wrote a JSONPath library for Java</title>
      <dc:creator>christoph-sens</dc:creator>
      <pubDate>Sat, 10 Oct 2026 10:56:09 +0000</pubDate>
      <link>https://dev.to/christophsens/treetrail-why-i-wrote-a-jsonpath-library-for-java-1g0n</link>
      <guid>https://dev.to/christophsens/treetrail-why-i-wrote-a-jsonpath-library-for-java-1g0n</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.christoph-sens.com/blog/why-i-wrote-treetrail-jsonpath-for-java/" rel="noopener noreferrer"&gt;christoph-sens.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you work with JSON in Java, you use JSONPath – often without noticing. In Spring tests it sits behind &lt;code&gt;jsonPath("$.items[0].name")&lt;/code&gt;, in API gateways and integration platforms it picks fields out of webhooks, and in configuration files it describes which value goes where. Surprisingly, JSONPath had no specification for some 17 years after Stefan Goessner's article of 2007. Every library decided for itself what &lt;code&gt;$..book[-1:]&lt;/code&gt; or &lt;code&gt;$[?(@.price &amp;lt; 10)]&lt;/code&gt; should return.&lt;/p&gt;

&lt;p&gt;That changed in February 2024, when the IETF published &lt;a href="https://www.rfc-editor.org/rfc/rfc9535" rel="noopener noreferrer"&gt;RFC 9535&lt;/a&gt;, a standard for JSONPath, together with a &lt;a href="https://github.com/jsonpath-standard/jsonpath-compliance-test-suite" rel="noopener noreferrer"&gt;Compliance Test Suite&lt;/a&gt; of 706 test cases. Conformant implementations soon appeared for Python, Rust and .NET. For Java – the platform that probably runs more JSONPath expressions than any other – a mature answer was missing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/treetrail/treetrail" rel="noopener noreferrer"&gt;Treetrail&lt;/a&gt; is my attempt to close that gap. This post explains why the library exists, what it can do today and how it compares with the alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The motivation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One query, several answers
&lt;/h3&gt;

&lt;p&gt;It started with a simple observation: the same JSONPath query returns different results depending on the library. On the array &lt;code&gt;[0, 1, …, 9]&lt;/code&gt;, for example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;RFC 9535&lt;/th&gt;
&lt;th&gt;Jayway JsonPath 3.0.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$[1:6:2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1, 3, 5]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[1, 2, 3, 4, 5]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$[9:0:-1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[9, 8, 7, 6, 5, 4, 3, 2, 1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As long as an expression lives in a single Java service, this hardly shows. But once the same query is evaluated in the backend, in a Python script for data analysis and in a TypeScript frontend, it gets tricky: three components, three interpretations, and the bug only shows up in production. That is exactly what standards are for. A standard only helps, though, if every platform has an implementation that really follows it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jayway JsonPath: a great service, but older than the standard
&lt;/h3&gt;

&lt;p&gt;By far the most widely used JSONPath library in Java is &lt;a href="https://github.com/json-path/JsonPath" rel="noopener noreferrer"&gt;Jayway JsonPath&lt;/a&gt;. It has served the Java ecosystem extremely well since 2011, and Spring uses it for its &lt;code&gt;jsonPath(...)&lt;/code&gt; matchers. But it was created long before there was a standard – and so, through no fault of its own, it does not conform to it. Measured against the Compliance Test Suite, Jayway 3.0.0 returns the same result as the standard for 86 of the 459 valid queries; with filters put in parentheses, as Jayway requires, it is 176. Conversely, Jayway accepts 132 of the 247 queries that the standard considers invalid.&lt;/p&gt;

&lt;p&gt;A library this widely used cannot simply change its behavior without breaking thousands of projects. That calls for a separate, standard-conformant library – and for a way to switch to it without risk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Queries from untrusted sources
&lt;/h3&gt;

&lt;p&gt;The third reason is security. More and more often, JSONPath expressions don't come from your own code but from tenants, users or third-party configuration. Then a query must not be able to take the service down. Regular expressions with backtracking are the best-known weak spot: the pattern &lt;code&gt;((a+)+)+b&lt;/code&gt; against 24 &lt;code&gt;a&lt;/code&gt;s and a &lt;code&gt;!&lt;/code&gt; keeps &lt;code&gt;java.util.regex&lt;/code&gt; busy for about 0.4 seconds on JDK 25 – and every further &lt;code&gt;a&lt;/code&gt; doubles the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  And a personal reason
&lt;/h3&gt;

&lt;p&gt;I have been building backends with Java and Kotlin for more than 15 years. To me, open source is the most honest way to show how I work: every decision, every test and every bug is public. Treetrail is written in Java on purpose, not in Kotlin – a dependency on the Kotlin standard library would make adoption by projects such as Spring, Apache Camel or Apache NiFi needlessly harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Treetrail can do today
&lt;/h2&gt;

&lt;p&gt;Version 0.2.0 has been on Maven Central since 8 October 2026. Its key features:&lt;/p&gt;

&lt;h3&gt;
  
  
  Standard-conformant – checked in every build
&lt;/h3&gt;

&lt;p&gt;Treetrail passes all 706 cases of the Compliance Test Suite, on Java 17, 21 and 25 and with every supported JSON model. If a single case fails, the build fails. Where the standard leaves room or the test suite deviates from the RFC text, the decision is documented in &lt;a href="https://github.com/treetrail/treetrail/blob/main/docs/conformance.md" rel="noopener noreferrer"&gt;docs/conformance.md&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  One kind of result
&lt;/h3&gt;

&lt;p&gt;Every query returns a list of nodes, whether it selects one value or many. Every node knows its normalized path. There is no configuration option that changes what a query returns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JsonPath&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$.store.book[?@.price &amp;lt; 10].title"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;NodeList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ["Sayings of the Century", "Moby Dick"]&lt;/span&gt;
&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// ["$['store']['book'][0]['title']", "$['store']['book'][2]['title']"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The JSON tree you already have
&lt;/h3&gt;

&lt;p&gt;The core has no dependencies besides the JDK. It works on plain Java maps and lists, or parses JSON text itself with a small, strict parser. Adapters query the trees of Jackson 2 and 3, Gson and Jakarta JSON-P directly – without conversion – and return that library's own nodes, so you can keep working with them. Other models plug in through a small interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JsonNode&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;objectMapper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readTree&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;NodeList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;JsonNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$.store.book[?@.price &amp;lt; 10]"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Jackson2Model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Safe with untrusted queries
&lt;/h3&gt;

&lt;p&gt;Regular expressions in &lt;code&gt;match()&lt;/code&gt; and &lt;code&gt;search()&lt;/code&gt; follow &lt;a href="https://www.rfc-editor.org/rfc/rfc9485" rel="noopener noreferrer"&gt;I-Regexp (RFC 9485)&lt;/a&gt; and run on Treetrail's own automaton engine without backtracking, in linear time. The pattern above that costs &lt;code&gt;java.util.regex&lt;/code&gt; 0.4 seconds takes Treetrail less than a tenth of a microsecond. On top of that, every run has limits: a budget of visited nodes, a maximum depth and a cap on the result size. Even if a document brings thousands of different regular expressions along, the caches hold about 10 MB at most.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;JsonPath&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;untrustedExpression&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withLimits&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EvaluationLimits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFAULT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withMaxVisitedNodes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;withMaxResultSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fast
&lt;/h3&gt;

&lt;p&gt;Conformance doesn't cost speed. In the JMH benchmarks, Treetrail is at least on par with Jayway JsonPath in every query; on Jackson trees, the most common setup in practice, it is 1.2 to 2.2 times faster, and 1.7 times faster at compiling an expression. The measurements and their caveats are in &lt;a href="https://github.com/treetrail/treetrail/blob/main/docs/benchmarks.md" rel="noopener noreferrer"&gt;docs/benchmarks.md&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Made for tests
&lt;/h3&gt;

&lt;p&gt;Most JSONPath code in Java projects lives in tests. Two modules bring the standard there: AssertJ assertions and a replacement for Spring's &lt;code&gt;jsonPath(...)&lt;/code&gt; matchers in MockMvc and WebTestClient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;mockMvc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;perform&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/store"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;andExpect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"$.store.book[?@.price &amp;lt; 10].title"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sayings of the Century"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Moby Dick"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Values compare as JSON values: &lt;code&gt;399&lt;/code&gt;, &lt;code&gt;399L&lt;/code&gt; and &lt;code&gt;399.0&lt;/code&gt; all match the JSON number &lt;code&gt;399&lt;/code&gt;. A failing assertion names the expression and shows the values it found, with their paths.&lt;/p&gt;

&lt;h3&gt;
  
  
  An orderly way off Jayway
&lt;/h3&gt;

&lt;p&gt;Nobody likes to migrate on a hunch. That's why Treetrail comes with two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonpath-migration&lt;/code&gt;&lt;/strong&gt; runs your expressions with Jayway JsonPath and with Treetrail against sample documents and reports every difference – with a rewrite hint for Jayway-specific syntax. It uses the Jayway version your project already depends on and takes your Jayway configuration into account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jsonpath-rewrite&lt;/code&gt;&lt;/strong&gt; contains an &lt;a href="https://docs.openrewrite.org" rel="noopener noreferrer"&gt;OpenRewrite&lt;/a&gt; recipe that finds every JSONPath expression in a code base that is passed to Jayway or to Spring's matchers, and assesses it: valid RFC 9535, Jayway-only, write API, or computed at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The README also has a &lt;a href="https://github.com/treetrail/treetrail#jayway-idioms-and-their-equivalents" rel="noopener noreferrer"&gt;table of Jayway idioms and their equivalents&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Treetrail compares
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compared with Jayway JsonPath
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Treetrail&lt;/th&gt;
&lt;th&gt;Jayway JsonPath&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;RFC 9535, all 706 CTS cases&lt;/td&gt;
&lt;td&gt;its own dialect, older than the standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result of &lt;code&gt;$.a.b&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;always a node list with paths&lt;/td&gt;
&lt;td&gt;a single value or a list, depending on path and configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filters&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;[?@.price &amp;lt; 10]&lt;/code&gt; (parentheses optional)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[?(@.price &amp;lt; 10)]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regular expressions&lt;/td&gt;
&lt;td&gt;I-Regexp, linear time&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;java.util.regex&lt;/code&gt; with backtracking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Limits for untrusted queries&lt;/td&gt;
&lt;td&gt;node budget, depth, result size, regex memory&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON libraries&lt;/td&gt;
&lt;td&gt;Java objects, Jackson 2/3, Gson, JSON-P&lt;/td&gt;
&lt;td&gt;json-smart, Jackson, Gson and others via providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spring test matchers&lt;/td&gt;
&lt;td&gt;separate module&lt;/td&gt;
&lt;td&gt;built into Spring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write API (&lt;code&gt;set&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;no, queries only&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type mapping (&lt;code&gt;read(…, Integer.class)&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;no, via your own JSON library&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate functions (&lt;code&gt;sum()&lt;/code&gt;, &lt;code&gt;avg()&lt;/code&gt;, …)&lt;/td&gt;
&lt;td&gt;no; standard functions &lt;code&gt;length()&lt;/code&gt;, &lt;code&gt;count()&lt;/code&gt;, &lt;code&gt;match()&lt;/code&gt;, &lt;code&gt;search()&lt;/code&gt;, &lt;code&gt;value()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table also shows where Jayway offers more: the write API, type mapping and aggregate functions are convenient and deliberately not part of the standard. If you rely on them heavily, the README shows how to do the same with Treetrail and your own JSON library – or you keep Jayway for those places. The two libraries can live side by side without trouble.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compared with other RFC 9535 implementations in Java
&lt;/h3&gt;

&lt;p&gt;Treetrail is not the only Java library that implements the standard, and that's a good thing. Christoph Burgmer's &lt;a href="https://github.com/cburgmer/json-path-comparison" rel="noopener noreferrer"&gt;JSONPath Comparison&lt;/a&gt; has compared dozens of implementations across all languages for years. I have submitted a &lt;a href="https://github.com/cburgmer/json-path-comparison/pull/170" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; for Treetrail there; in a local run over 258 queries, the picture for Java looks like this (agreement with the cross-language consensus on 175 queries):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;th&gt;Consensus matched&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Treetrail&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ajp (based on XSLT/ixml)&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SJF4J&lt;/td&gt;
&lt;td&gt;173&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jayway JsonPath 3.0.0&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSurfer&lt;/td&gt;
&lt;td&gt;133&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two remaining queries are shorthand forms that are invalid under RFC 9535 – there the standard deliberately departs from the old consensus. What I find remarkable is that Treetrail and ajp, two entirely independent implementations, answer all 258 queries identically. That is exactly what a standard is supposed to achieve.&lt;/p&gt;

&lt;p&gt;So the differences lie less in conformance than in everything around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SJF4J&lt;/strong&gt; is more lenient and accepts considerably more expressions that the standard rejects – handy when switching, but less strict if queries are meant to stay portable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/MarcusDunn/jsonlens" rel="noopener noreferrer"&gt;jsonlens&lt;/a&gt;&lt;/strong&gt; combines JSONPath with JSON Pointer (RFC 6901) and JSON Patch (RFC 6902) – features Treetrail doesn't have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treetrail&lt;/strong&gt; focuses on operational safety with untrusted queries, direct adapters for the common JSON libraries, test integration for AssertJ and Spring, and migration tools for existing Jayway code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Treetrail is tested
&lt;/h2&gt;

&lt;p&gt;The Compliance Test Suite checks grammar and semantics, but not what happens with inputs nobody thought of. So on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzing&lt;/strong&gt; with &lt;a href="https://github.com/CodeIntelligenceTesting/jazzer" rel="noopener noreferrer"&gt;Jazzer&lt;/a&gt; in every CI build and every night: compiling, querying, regex matching and JSON parsing may only throw the documented exceptions and must finish each input within five seconds. The fuzzer did find a bug before 0.2.0: a regular expression that repeated an empty group more than a billion times took 46 seconds to compile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property tests&lt;/strong&gt; with &lt;a href="https://jqwik.net" rel="noopener noreferrer"&gt;jqwik&lt;/a&gt;, for example that every normalized path selects exactly its node.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Differential tests:&lt;/strong&gt; 20,000 random regular expressions against &lt;code&gt;java.util.regex&lt;/code&gt;, and 2,000 random queries against &lt;a href="https://github.com/jg-rp/python-jsonpath-rfc9535" rel="noopener noreferrer"&gt;jsonpath-rfc9535&lt;/a&gt;, a careful Python implementation of the standard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency:&lt;/strong&gt; 16 threads share compiled queries while the regex automata are being built.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The supply chain is part of quality too: every release is signed and ships a CycloneDX SBOM and a build provenance attestation per module, and the jars are reproducible. Recently, the build has also started checking API compatibility with the last release, test coverage per module, and the code itself with Error Prone and NullAway; the next release brings &lt;a href="https://jspecify.dev" rel="noopener noreferrer"&gt;JSpecify&lt;/a&gt; annotations, so Kotlin sees which values can be &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The road to 1.0 is planned &lt;a href="https://github.com/treetrail/treetrail/milestones" rel="noopener noreferrer"&gt;in the open on GitHub&lt;/a&gt;: a stable API with a compatibility guarantee, more static analysis and mutation testing. Before 1.0 the API may still change – which makes now the best time for feedback.&lt;/p&gt;

&lt;p&gt;Trying it takes one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.treetrail:jsonpath-core:0.2.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Treetrail runs on Java 17 and later and is licensed under the Apache License 2.0. If something is missing or doesn't fit your use case, please &lt;a href="https://github.com/treetrail/treetrail/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. And if your team is wondering how to move JSONPath expressions in a grown code base safely to the standard: &lt;a href="mailto:contact@christoph-sens.com"&gt;get in touch&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>jsonpath</category>
      <category>opensource</category>
      <category>spring</category>
    </item>
    <item>
      <title>Large SQS and SNS messages in Kotlin: the extended client pattern without Java baggage</title>
      <dc:creator>christoph-sens</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:39:40 +0000</pubDate>
      <link>https://dev.to/christophsens/large-sqs-and-sns-messages-in-kotlin-the-extended-client-pattern-without-java-baggage-l9d</link>
      <guid>https://dev.to/christophsens/large-sqs-and-sns-messages-in-kotlin-the-extended-client-pattern-without-java-baggage-l9d</guid>
      <description>&lt;p&gt;Every SQS queue and SNS topic has a message size limit. When a payload is larger, the usual answer is the &lt;em&gt;claim check&lt;/em&gt; pattern: put the payload in S3, send a small pointer instead, and resolve the pointer on the receiving side. AWS ships this pattern as two Java libraries, &lt;a href="https://github.com/awslabs/amazon-sqs-java-extended-client-lib" rel="noopener noreferrer"&gt;amazon-sqs-java-extended-client-lib&lt;/a&gt;and &lt;a href="https://github.com/awslabs/amazon-sns-java-extended-client-lib" rel="noopener noreferrer"&gt;amazon-sns-java-extended-client-lib&lt;/a&gt;, both built on &lt;a href="https://github.com/awslabs/payload-offloading-java-common-lib-for-aws" rel="noopener noreferrer"&gt;payload-offloading-java-common-lib-for-aws&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If your service is written in Kotlin on &lt;a href="https://github.com/awslabs/aws-sdk-kotlin" rel="noopener noreferrer"&gt;aws-sdk-kotlin&lt;/a&gt;,&lt;br&gt;
those libraries don't fit: they wrap the AWS SDK for Java v2 clients, not the Kotlin ones. So I&lt;br&gt;
ported them. The result is three small libraries on Maven Central:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/christoph-sens/s3overflow" rel="noopener noreferrer"&gt;s3overflow&lt;/a&gt;: the payload store (S3 upload, pointer, download, delete)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/christoph-sens/sqsoverflow" rel="noopener noreferrer"&gt;sqsoverflow&lt;/a&gt;: an &lt;code&gt;SqsClient&lt;/code&gt; that offloads large message bodies&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/christoph-sens/snsoverflow" rel="noopener noreferrer"&gt;snsoverflow&lt;/a&gt;: an &lt;code&gt;SnsClient&lt;/code&gt; that offloads large message bodies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers why the Java libraries are a liability today, how the ports work, and what you&lt;br&gt;
need to know before migrating.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where the Java libraries stand today
&lt;/h2&gt;

&lt;p&gt;The extended clients do their job, but their foundation has aged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Old dependencies with known advisories.&lt;/strong&gt; Both clients depend on &lt;code&gt;payloadoffloading-common&lt;/code&gt; 2.2.0, whose last release was in March 2024. It pins &lt;code&gt;jackson-databind&lt;/code&gt; 2.15.2 and &lt;code&gt;jackson-core&lt;/code&gt; 2.16.0. As of September 2026, the GitHub Advisory Database lists seven advisories affecting exactly these versions, three of them rated high, among them bypasses of Jackson's polymorphic type validation and a denial-of-service issue in the async parser. Even the latest SQS client release (2.1.3, July 2026) still pulls in these versions  through the common library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java 8 bytecode and a pre-coroutine design.&lt;/strong&gt; 
All three libraries compile for Java 8 and keep separate sync and async client classes with largely duplicated pass-through code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow release cadence.&lt;/strong&gt; 
The SNS client's latest release (2.1.0) is from March 2024, the common library's from the same month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be fair: whether an advisory is exploitable depends on how Jackson is used, and the extended clients only (de)serialize a small pointer object. You can also override the Jackson versions in your own build. But every dependency scanner flags these versions, someone has to triage the findings again and again, and forcing newer Jackson versions under a library that was never tested against them is its own risk. Removing Jackson from this code path makes the question disappear.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the size limits are today
&lt;/h2&gt;

&lt;p&gt;The limits moved recently, which changes when offloading is needed at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt; accepts messages up to &lt;strong&gt;1 MiB&lt;/strong&gt; (body plus attributes). Until August 2025 the limit was 256 KiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SNS&lt;/strong&gt; accepts &lt;strong&gt;256 KiB&lt;/strong&gt; by default. Since September 2026 a topic can accept up to &lt;strong&gt;1 MiB&lt;/strong&gt; when you raise its &lt;code&gt;MaximumMessageSize&lt;/code&gt; attribute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;sqsoverflow therefore offloads at 1 MiB by default, snsoverflow at 256 KiB. Both are configurable&lt;br&gt;
through &lt;code&gt;payloadSizeThreshold&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.christoph-sens:sqsoverflow:1.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;s3Client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnvironment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"eu-central-1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sqsClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SqsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnvironment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"eu-central-1"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SqsExtendedClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sqsClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;SqsExtendedClientConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadStore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3BackedPayloadStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-payload-bucket"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SendMessageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;queueUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myQueueUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;messageBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;largePayload&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receiveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReceiveMessageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;queueUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myQueueUrl&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// the original payload, already resolved from S3&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DeleteMessageRequest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;queueUrl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myQueueUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;receiptHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;receiptHandle&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;SqsExtendedClient&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; an &lt;code&gt;SqsClient&lt;/code&gt;, so you can pass it anywhere a plain client is expected.&lt;br&gt;
Messages under the threshold go straight to SQS. Larger ones are written to S3, and SQS only carries&lt;br&gt;
a small JSON pointer plus the &lt;code&gt;ExtendedPayloadSize&lt;/code&gt; message attribute. On receive, the client fetches the payload and hands you the original body. On delete, it removes the S3 object as well (&lt;code&gt;cleanupS3Payload&lt;/code&gt;, on by default).&lt;/p&gt;

&lt;p&gt;snsoverflow works the same way for &lt;code&gt;publish&lt;/code&gt; and &lt;code&gt;publishBatch&lt;/code&gt;. SNS has no receive side, so the&lt;br&gt;
subscriber resolves the pointer: for an SNS topic that fans out to an SQS queue with raw message delivery, that's sqsoverflow's &lt;code&gt;SqsExtendedClient&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why a port instead of a wrapper around the Java library
&lt;/h2&gt;
&lt;h3&gt;
  
  
  One suspend API instead of sync and async classes
&lt;/h3&gt;

&lt;p&gt;The Java originals come in two flavors each, for example &lt;code&gt;AmazonSQSExtendedClient&lt;/code&gt; for &lt;code&gt;SqsClient&lt;/code&gt; and &lt;code&gt;AmazonSQSExtendedAsyncClient&lt;/code&gt; for &lt;code&gt;SqsAsyncClient&lt;/code&gt;. aws-sdk-kotlin clients are &lt;code&gt;suspend&lt;/code&gt;-based from the start, so one class covers both cases and fits naturally into coroutine code.&lt;/p&gt;
&lt;h3&gt;
  
  
  Interface delegation replaces a thousand lines of pass-through code
&lt;/h3&gt;

&lt;p&gt;Most of the SQS interface has nothing to do with payload offloading: &lt;code&gt;createQueue&lt;/code&gt;, &lt;code&gt;listQueues&lt;/code&gt;, &lt;code&gt;tagQueue&lt;/code&gt; and dozens more. The Java library forwards each of these by hand in a base class of roughly 1,150 lines. Kotlin's interface delegation does this in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SqsExtendedClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sqsClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SqsClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clientConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SqsExtendedClientConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SqsClient&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;sqsClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SendMessageRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;SendMessageResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* offload if needed */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... only the methods with real offload logic are overridden&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sqsoverflow overrides eight methods: &lt;code&gt;sendMessage&lt;/code&gt;, &lt;code&gt;sendMessageBatch&lt;/code&gt;, &lt;code&gt;receiveMessage&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;deleteMessage&lt;/code&gt;, &lt;code&gt;deleteMessageBatch&lt;/code&gt;, &lt;code&gt;changeMessageVisibility&lt;/code&gt;, &lt;code&gt;changeMessageVisibilityBatch&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;purgeQueue&lt;/code&gt;. snsoverflow overrides two: &lt;code&gt;publish&lt;/code&gt; and &lt;code&gt;publishBatch&lt;/code&gt;. Everything else is delegated&lt;br&gt;
to the wrapped client. All three libraries together are about 730 lines of Kotlin, license headers&lt;br&gt;
included.&lt;/p&gt;
&lt;h3&gt;
  
  
  No Jackson dependency
&lt;/h3&gt;

&lt;p&gt;The Java libraries serialize the S3 pointer with Jackson. The ports use kotlinx.serialization, so nothing in their runtime classpath is Jackson, and the advisories described above do not apply to them. Fewer transitive dependencies means fewer libraries to keep patched. Dependabot keeps the&lt;br&gt;
remaining dependencies current, and CodeQL and dependency review run on every pull request in all&lt;br&gt;
three repositories.&lt;/p&gt;
&lt;h3&gt;
  
  
  Less configuration surface
&lt;/h3&gt;

&lt;p&gt;The original configuration classes carry options for client-side encryption strategies and canned ACLs. The ports take a &lt;code&gt;PayloadStore&lt;/code&gt; and a handful of behavior flags (&lt;code&gt;payloadSizeThreshold&lt;/code&gt;, &lt;code&gt;alwaysThroughS3&lt;/code&gt;, &lt;code&gt;cleanupS3Payload&lt;/code&gt;, &lt;code&gt;ignorePayloadNotFound&lt;/code&gt;, &lt;code&gt;s3KeyPrefix&lt;/code&gt;). Encryption is configured where it belongs: on the bucket, with SSE-S3 or SSE-KMS.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;publishBatch&lt;/code&gt; in snsoverflow is offload-aware as well. The Java SNS library predates the SNS &lt;code&gt;PublishBatch&lt;/code&gt; API and only handles &lt;code&gt;publish&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Migrating from the Java libraries
&lt;/h2&gt;

&lt;p&gt;The important caveat first: &lt;strong&gt;the ports are not wire-compatible with the Java libraries.&lt;/strong&gt; The pointer JSON and the receipt-handle format differ. A message sent by the Java extended client cannot be resolved by sqsoverflow, and vice versa. Switch all producers and consumers of a queue at the&lt;br&gt;
same time, or drain the queue first.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ExtendedPayloadSize&lt;/code&gt; attribute name is the same, so SNS-to-SQS fan-out works between snsoverflow and sqsoverflow.&lt;/p&gt;

&lt;p&gt;Before (Java):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;ExtendedClientConfiguration&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExtendedClientConfiguration&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withPayloadSupportEnabled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3Client&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"my-payload-bucket"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;SqsClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AmazonSQSExtendedClient&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SqsClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (Kotlin):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SqsExtendedClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;SqsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEnvironment&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nc"&gt;SqsExtendedClientConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payloadStore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;S3BackedPayloadStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s3Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-payload-bucket"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Options without an equivalent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Java option&lt;/th&gt;
&lt;th&gt;In the ports&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client-side encryption (&lt;code&gt;ServerSideEncryptionStrategy&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Configure SSE-S3 or SSE-KMS on the bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ObjectCannedACL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Use bucket policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SNS per-message &lt;code&gt;"S3Key"&lt;/code&gt; attribute&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;s3KeyPrefix&lt;/code&gt; in the config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy &lt;code&gt;SQSLargePayloadSize&lt;/code&gt; attribute name&lt;/td&gt;
&lt;td&gt;Always &lt;code&gt;ExtendedPayloadSize&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Verifying what you download
&lt;/h2&gt;

&lt;p&gt;Every release is built and published by a GitHub Actions workflow. The jars, POM and Gradle module metadata on Maven Central carry a signed &lt;a href="https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations" rel="noopener noreferrer"&gt;build provenance attestation&lt;/a&gt;that ties each file to the repository, the workflow and the tagged commit it was built from. You can check a downloaded jar with the GitHub CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh attestation verify sqsoverflow-1.1.0.jar &lt;span class="nt"&gt;--repo&lt;/span&gt; christoph-sens/sqsoverflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Licensing
&lt;/h2&gt;

&lt;p&gt;sqsoverflow and snsoverflow are derivative works of the AWS libraries under Apache-2.0, with per-file attribution and a NOTICE file listing what was changed. s3overflow is an independent reimplementation that takes no code from &lt;code&gt;payload-offloading-java-common-lib-for-aws&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;s3overflow: &lt;a href="https://github.com/christoph-sens/s3overflow" rel="noopener noreferrer"&gt;https://github.com/christoph-sens/s3overflow&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;sqsoverflow: &lt;a href="https://github.com/christoph-sens/sqsoverflow" rel="noopener noreferrer"&gt;https://github.com/christoph-sens/sqsoverflow&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;snsoverflow: &lt;a href="https://github.com/christoph-sens/snsoverflow" rel="noopener noreferrer"&gt;https://github.com/christoph-sens/snsoverflow&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues and pull requests are welcome. If you run into a case the libraries don't cover yet, such as another AWS service that could use the same pattern, open an issue.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm a freelance developer and consultant working on Kotlin, Java and AWS. The story of how these libraries came about is on my website (in German):&lt;br&gt;
&lt;a href="https://www.christoph-sens.com/post/der-scan-befund-der-nie-verschwindet-wie-eine-ki-drei-aws-legacy-bibliotheken-an-einem-nachmittag" rel="noopener noreferrer"&gt;1150 Zeilen Legacy, eine Zeile Kotlin&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>aws</category>
      <category>opensource</category>
      <category>java</category>
    </item>
  </channel>
</rss>
