Skip to content

fix(deps): bump: bump com.cedarsoftware:json-io from 4.93.0 to 4.101.0#395

Open
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/gradle/main/com.cedarsoftware-json-io-4.101.0
Open

fix(deps): bump: bump com.cedarsoftware:json-io from 4.93.0 to 4.101.0#395
dependabot[bot] wants to merge 1 commit intomainfrom
dependabot/gradle/main/com.cedarsoftware-json-io-4.101.0

Conversation

@dependabot
Copy link
Copy Markdown
Contributor

@dependabot dependabot Bot commented on behalf of github Apr 20, 2026

Bumps com.cedarsoftware:json-io from 4.93.0 to 4.101.0.

Release notes

Sourced from com.cedarsoftware:json-io's releases.

4.101.0

json-io 4.101.0

Maven Central: com.cedarsoftware:json-io:4.101.0 (requires java-util 4.101.0+)

Highlights

New APIs

  • WriteOptionsBuilder.standardJson() — one-call convenience setting showTypeInfoNever(), showRootTypeInfo(false), cycleSupport(false), stringifyMapKeys(true), useMetaPrefixDollar(), and ISO-8601 date formatting. Produces Jackson-compatible JSON for normal POJO/List/Map cases. Chainable. Will become the default in 5.0.0.
  • WriteOptionsBuilder.namingStrategy(IoNaming.Strategy) — global naming strategy applied to all classes without a per-class @IoNaming/@JsonNaming. Priority: per-field @IoProperty/@JsonProperty > per-class annotation > global strategy > field name as-is. Two new enum values: UPPER_SNAKE_CASE and LOWER_CASE (full set: SNAKE_CASE, UPPER_SNAKE_CASE, KEBAB_CASE, UPPER_CAMEL_CASE, LOWER_DOT_CASE, LOWER_CASE).
  • WriteOptionsBuilder.stringifyMapKeys(boolean) — non-String map keys with bidirectional String conversion (Long, UUID, Enum, Date, etc.) written as plain JSON object keys instead of @keys/@items parallel-array form. Default false for JSON (backward-compat), true for JSON5.
  • WriteOptionsBuilder.preserveLeafContainerIdentity(boolean) — when false (the new default), traceReferences skips containers holding only non-referenceable leaves (List<String>, Map<UUID, Date>, String[], etc.), matching Jackson's default identity semantics.
  • WriteOptionsBuilder.writeOptionalAsObject(boolean) — set to true for legacy {"present":true,"value":X} form; default false writes Optionals as bare primitive values.
  • 1-arg convenience overloads: JsonIo.toJson(obj), toToon(obj), toJava(json), toJava(stream), fromToon(toon), fromToon(stream).

Performance

  • Full-suite performance pass — all 8 Read/Write ratios under 2.0x vs Jackson on a warm JVM (best 1.32x JsonIo Read toMaps, worst ~1.97x Toon Read toJava).
  • Switched all hot-path ClassValueMap/ClassValueSet lookups to the new getByClass(Class) / containsClass(Class) fast-path APIs (added in java-util 4.101.0).
  • AnnotationResolver.getMetadata — split into trivially-inlineable fast path + scanAndCache cold path. Fast-path self-time dropped ~50%.
  • ObjectResolver.traverseFields — hoisted per-object AnnotationResolver.getMetadata call out of the per-field loop (~20% of Resolver-phase samples).
  • ReadOptions.isNonReferenceableClass / isNotCustomReaderClass — memoized via per-instance ClassValue<Boolean> caches (mirrors WriteOptions.nonRefCache).
  • JsonWriter.traceReferences — filters non-referenceable leaves at push site (save deque push+pop per leaf). JsonIo Write cycle=true toJava −5.8%, toMaps −7.5%.
  • ToonWriter — precomputed key-quoting decision at WriteFieldPlan build time (eliminates per-field ConcurrentHashMap.get — 11.7% of ToonWriter samples). Toon Write cycle=false toJava −5.0%.
  • JsonWriter.writeField / ToonWriter.writeFieldEntry — primitive+String fast paths bypass full dispatch for the most common field-value types.
  • ToonWriter.writeMap — skips hasComplexKeys iteration when the enclosing field's declared Map key type is Converter-stringifiable.
  • JsonWriter.writeCollectionElement — POJO short-circuit for cycleSupport=false collapses 4-level dispatch to 2 levels.

Bug fixes

  • Optional, OptionalInt, OptionalLong, OptionalDouble — all four now write Jackson/Gson-compatible primitive form by default (Optional.empty()null, Optional.of(X)X). Prior behavior emitted invalid JSON for Optional ({null} or {"hello"}) and broken POJO form for the three primitive Optionals. Field-type-aware coercion wraps bare scalars into the appropriate Optional variant on read. Reader accepts both new and legacy object form for backward compatibility. Reported by Jim Ronan (*/dxg Health).
  • JsonWriter.writeLongDirect() — inner digit-extraction loop used int q which silently truncated Long.MIN_VALUE (rendered as -206158430208 instead of -9223372036854775808). Fixed by using long q.

Jackson-alignment (opt-in)

  • WriteOptionsBuilder.standardJson() and json5() now also configure ISO-8601 date formatting for java.util.Date / java.sql.Date. Matches Spring Boot's default Jackson configuration (WRITE_DATES_AS_TIMESTAMPS=false).

Full changelog: https://github.com/jdereg/json-io/blob/master/changelog.md

4.100.0 - 2026-04-10

Bug Fixes

  • Fixed deadlock from circular static initialization between ReadOptionsBuilder and WriteOptionsBuilder. When two threads concurrently triggered class loading (e.g., one calling JsonIo.toJson() and another calling JsonIo.toJava()), the JVM's class initialization locks would deadlock. Broke the cycle by having each builder load its configuration independently and by removing the circular dependency through MetaUtils bootstrap methods.

Dependency Updates

  • java-util 4.100.0 (includes fix for unbounded Converter.FULL_CONVERSION_CACHE memory leak)
  • JUnit 5.14.2 → 5.14.3
  • Jackson 2.21.1 → 2.21.2

4.99.0 - 2026-03-28

... (truncated)

Changelog

Sourced from com.cedarsoftware:json-io's changelog.

4.101.0 - 2026-04-19

  • PERFORMANCE: Switched isNonReferenceableClass / isNotCustomReaderClass / isNotCustomWrittenClass membership tests to the new ClassValueSet.containsClass(Class) fast path (added in java-util 4.101.0). The ClassValue<Boolean> caches (nonRefCache on read and write sides, notCustomReadCache on the read side) call into the underlying nonRefClasses / notCustomReadClasses / notCustomWrittenClasses sets on first-touch per class, and isNotCustomWrittenClass is also called outside the cache on the JSON write hot path. Previously these routed through Set.contains(Object) which re-runs the instanceof Class guard and (post-build()) goes through the unmodifiableView delegating wrapper; now they call ClassValueSet.containsClass(Class) directly. Because build() reassigns the Set<Class<?>> field to an unmodifiable view (losing the ClassValueSet static type), a typed fast-path reference (nonRefClassesFast / notCustomReadClassesFast / notCustomWrittenClassesFast) is captured at the top of build() before the view reassignment — the hot-path computeValue reads that typed reference when non-null, falling back to the old Set.contains when it isn't (builder-internal callers pre-build()). No external API change, no semantic change.
  • PERFORMANCE: Switched hot-path ClassValueMap.get(Object) call sites to the new ClassValueMap.getByClass(Class) fast path (added in java-util 4.101.0). get(Object) must guard against non-Class keys (falling through to the ConcurrentHashMap backing) before routing to the ClassValue cache; getByClass(Class) skips that guard and compiles to a near-direct ClassValue.get(type) call. Converted sites: AnnotationResolver.getMetadata class-lookup, Resolver.DEFAULT_INSTANTIATORS + SCALAR_KINDS dispatch, Injector.numericKind + primitiveWrapper, JsonWriter.PRIM_ARRAY_WRITERS dispatch, ReadOptionsBuilder.getDeepDeclaredFields / getDeepInjectorMap / getInjectorPlan / BASE_NONSTANDARD_SETTERS lookup, WriteOptionsBuilder.getCustomWriter / getCustomWriterGate / getWriteFieldPlansForClass, and Converter.ClassPairMap outer-tier lookups. Field declarations that stored a ClassValueMap behind a Map<Class<?>, V> type were tightened to ClassValueMap<V> so the compiler resolves the typed call statically. No semantic change; all 3806 tests pass.
  • PERFORMANCE: AnnotationResolver.getMetadata — split the single method into a tiny fast-path and a separate cold-path helper (scanAndCache). The original kept the cache-hit fast path and the cache-miss cycle-guard + try/finally + scan() + cache.put work in one method; the try/finally bytecode inflated method size and suppressed HotSpot's inlining at the many hot call sites in ObjectResolver / WriteOptionsBuilder / WriteFieldPlan. After the split, getMetadata is just a null check + cache.get + ternary delegation — trivially inlineable. IntelliJ async-profiler confirmation: the phantom "5090 ms on the finally-line" attribution disappeared (it was recursive-scan time being pinned on the outer try/finally edge by fuzzy line-number mapping), and fast-path bucket dropped from ~600 ms to ~290 ms (~50% reduction in hot-path self-time). Pure structural refactor — no semantic change.
  • PERFORMANCE: ObjectResolver.traverseFields / assignField / assignJsonObjectField — hoisted the per-object AnnotationResolver.getMetadata(target.getClass()) lookup out of the per-field loop. Every POJO field assignment used to re-derive parentMeta from target.getClass() (in assignField at the field-deserialize-override check, and again in assignJsonObjectField for the @IoDeserialize/@​IoTypeInfo dispatch), even though the metadata is invariant across all fields of the same object and already cached in a ClassValueMap. JFR showed AnnotationResolver.getMetadata + its downstream ClassValueMap.get totaling ~20% of the Resolver-phase execution samples on toJava reads — the single biggest bucket outside scalar coercion. Capturing parentMeta once at the top of traverseFields and threading it through as an explicit parameter eliminates those redundant calls while also making the shared-invariant dependency visible at the call boundary where it's provably valid. With the profiler disabled, the 3-run median result is all 8 Read/Write ratios below 2.0x vs Jackson (best 1.32x on JsonIo Read toMaps, worst 1.97x on Toon Read toJava; previously Toon Read toJava was 2.06x and JsonIo Read toJava was 2.02x, both borderline). No semantic change — pure call-hoisting refactor. All 3806 tests pass.
  • FEATURE: New WriteOptionsBuilder.namingStrategy(IoNaming.Strategy) — a global naming strategy applied to all classes that lack a per-class @IoNaming / @JsonNaming annotation and all fields that lack a per-field @IoProperty / @JsonProperty rename. Priority at field-name resolution time: per-field @IoProperty/@JsonProperty > per-class @IoNaming/@JsonNaming > global namingStrategy(...) > Java field name as-is. This is the opt-in equivalent of Jackson's ObjectMapper.setPropertyNamingStrategy(...), letting Jackson users migrate to json-io without annotating every DTO. Two new IoNaming.Strategy enum values fill the gap with Jackson's PropertyNamingStrategies: UPPER_SNAKE_CASE (firstNameFIRST_NAME) and LOWER_CASE (firstNamefirstname); full set is now SNAKE_CASE, UPPER_SNAKE_CASE, KEBAB_CASE, UPPER_CAMEL_CASE, LOWER_DOT_CASE, LOWER_CASE. Default is null (no global strategy; behavior unchanged for existing users). Also added addPermanentNamingStrategy(IoNaming.Strategy) for JVM-lifetime defaults, WriteOptions.getNamingStrategy() getter, and AnnotationResolver.applyNamingStrategy(...) is now public so external tooling can reuse the same transformation. Neither standardJson() nor json5() force a strategy — Jackson's own default is LowerCamelCase (Java field names), so imposing one would be a user-specific choice. 13 new tests in GlobalNamingStrategyTest cover all six strategies, per-field @IoProperty override, per-class @IoNaming / @JsonNaming override, copy-constructor carry-over, addPermanentNamingStrategy lifecycle, and standardJson() non-clobber behavior.
  • BUG FIX: Optional, OptionalInt, OptionalLong, and OptionalDouble no longer produce invalid JSON or garbage output.
    • Prior behavior: Optional fields written with showTypeInfoNever() (or when showType=false) emitted illegal fragments like {null} or {"hello"}. The primitive OptionalInt/Long/Double types had no custom writer/factory at all and were serialized as broken POJOs ({"isPresent":null,"value":null}) in every mode. TOON output was similarly broken for all four types.
    • New behavior: all four Optional types are now written in Jackson/Gson-compatible primitive form by default — Optional.empty()null, Optional.of(X)X (as a bare value). Works for JSON, JSON5, and TOON.
    • New WriteOptionsBuilder.writeOptionalAsObject(boolean) toggle — set to true to emit the legacy json-io object form ({"present":true,"value":X} / {"present":false}) for interoperating with pre-4.101.0 json-io readers. Default is false (Jackson-style). Also available as addPermanentWriteOptionalAsObject(boolean) for JVM-lifetime defaults.
    • standardJson() always resets writeOptionalAsObject to false so the Jackson-compatible form is part of the "5.0 defaults" preset.
    • Reader accepts both formats: the Jackson primitive form AND the legacy object form (backward compatibility with JSON produced by older json-io versions).
    • Field-type-aware coercion: when a field is declared Optional<T> / OptionalInt / OptionalLong / OptionalDouble and the JSON value is a bare scalar or null, the value is wrapped into the appropriate Optional variant (with Converter coercion for the inner type when needed).
    • Top-level .asClass(Optional.class) / .asClass(OptionalInt.class) / etc. now wraps the parsed scalar (or null) into the requested Optional variant.
    • New writers: OptionalIntWriter, OptionalLongWriter, OptionalDoubleWriter. New factories: OptionalIntFactory, OptionalLongFactory, OptionalDoubleFactory. New aliases: OptionalInt, OptionalLong, OptionalDouble.
    • 32 new tests across OptionalFieldTest, OptionalPrimitiveVariantsTest, and OptionalToonTest covering: Jim Ronan's original reproduction, all four Optional types empty + present, POJO fields holding Optionals, round-trip with standardJson(), the writeOptionalAsObject(true) legacy form, writeOptionalAsObject(true).standardJson() (standardJson overrides the toggle), backward-compat reading of the legacy object form, Optional in a list, shared references within Optional via cycleSupport, TOON round-trip for all four types.
    • Reported by Jim Ronan (Divisional Software Architect, */dxg Health).
  • BUG FIX: JsonWriter.writeLongDirect() — the inner digit-extraction loop stored value / 100 in an int q that silently truncated to 32 bits. writeLongDirect was previously only called for int-sized IDs (@id/@ref), so the bug was latent; the new writeImpl primitive-wrapper fast path exercised it with arbitrary user Long values including Long.MIN_VALUE (which rendered as -206158430208 instead of -9223372036854775808). Fixed by using long q and casting only the remainder (always 0-99) to int.
  • FEATURE: WriteOptionsBuilder.standardJson() and json5() now also configure ISO-8601 date formatting for java.util.Date / java.sql.Date via isoDateFormat(). The library-wide default remains epoch-millis (DateAsLongWriter) for backward compatibility; only these two "Jackson-alignment" presets flip to ISO-8601. java.time.* types (Instant, LocalDate, LocalDateTime, ZonedDateTime, OffsetDateTime) were already ISO-8601 by default, so no change there. This matches the Spring Boot default Jackson configuration (WRITE_DATES_AS_TIMESTAMPS=false) — which is what roughly 90% of real-world Java webapps actually emit and what every major public JSON API (Stripe, GitHub, Google Cloud, AWS, etc.) uses. Chainable: .standardJson().longDateFormat() re-enables the legacy epoch-millis form for users who need it. 6 new tests in StandardJsonDateTest covering java.util.Date/java.time.* round-trip behavior, default vs. standardJson() output, and override chaining.
  • FEATURE: Added 1-argument convenience overloads: JsonIo.toJson(srcObject), JsonIo.toToon(srcObject), JsonIo.toJava(json), JsonIo.toJava(inputStream), JsonIo.fromToon(toon), and JsonIo.fromToon(inputStream). Each delegates to the 2-argument version with null options (default behavior). Enables the clean Quick Start examples shown in the README.
  • FEATURE: Added WriteOptionsBuilder.standardJson() convenience method. Sets showTypeInfoNever(), showRootTypeInfo(false), cycleSupport(false), stringifyMapKeys(true), and useMetaPrefixDollar() in one call — producing standard JSON output identical to Jackson for normal usage (POJOs, Lists, Maps with String/numeric/UUID/Enum keys). Uses $ prefix for any remaining metadata (e.g., $type, $keys for POJO key fallback). These will become the defaults in json-io 5.0.0. Chainable — individual settings can be overridden after this call (e.g., .standardJson().stringifyMapKeys(false)).
  • FEATURE: Added WriteOptionsBuilder.stringifyMapKeys(boolean) option. When enabled, non-String map keys that have a bidirectional String conversion via Converter (Long, Integer, Double, Boolean, BigDecimal, BigInteger, UUID, Date, ZonedDateTime, Enum, Character, etc.) are written as stringified keys in standard JSON object format (e.g., {"100": "value"} for Map<Long, String>) instead of the proprietary @keys/@items parallel-array format. Complex POJO keys without a bidirectional String conversion fall back to @keys/@items. forceMapOutputAsTwoArrays(true) overrides this setting. Default is false for JSON (backward compatibility with pre-4.94.0 readers), true for JSON5. Also added addPermanentStringifyMapKeys() for JVM-lifetime defaults. 14 new write-side tests covering stringify output, round-trips, defaults, and option precedence.
  • FEATURE / PERFORMANCE: New WriteOptionsBuilder.preserveLeafContainerIdentity(boolean) with matching addPermanentPreserveLeafContainerIdentity(boolean) for JVM-lifetime defaults. When false (the new default), traceReferences no longer visits containers whose declared element types are all non-referenceable leaf types — i.e., List<String>, Map<UUID, Date>, String[], byte[], Map<Long, BigDecimal>, List<MyEnum>, etc. Two fields that happen to point to the same List<String> instance are now serialized as two independent copies rather than emitting @id/@ref, matching Jackson's default identity semantics and aligning with json-io's convergence toward standard JSON output. Containers holding POJO elements (List<Foo>, Map<String, Foo>, Foo[]) still have identity traced — this option only governs leaf-element containers. Only affects cycleSupport=true writes (cycle=false path skips traceReferences entirely). Applied consistently across JsonWriter.canSkipContainerTrace, JsonWriter.processArray, and ToonWriter.canSkipContainerTrace. standardJson() resets this to false. Measured write-ratio improvements vs Jackson (single run, toMaps block most affected): JsonIo cycle=true toMaps 2.26x → 1.70x (-25%), JsonIo cycle=true toJava 2.00x → 1.71x (-15%), Toon cycle=true toJava 2.05x → 1.84x (-10%), Toon cycle=true toMaps 2.17x → 2.00x (-8%). cycleSupport=false and Read ratios unchanged. Three prior tests (FieldsTest.testFields, FieldsTest.testReconstituteFields, ByteArrayTest.testNestedInObject_withDuplicates_andFieldTypeMatchesObjectType) updated to use preserveLeafContainerIdentity(true) — they specifically assert shared-identity preservation for String[]/byte[], which is now opt-in. Reported by user during a perf review discussion of @id/@ref emission patterns.
  • PERFORMANCE: ToonWriter.writeMap — skip the hasComplexKeys() iteration when the enclosing field's declared Map key type is known "simple" (Converter-stringifiable). WriteFieldPlan now pre-computes a mapKeyTypeIsSimple boolean at plan-build time via Converter.isSimpleTypeConversionSupported(declaredKeyType, String.class) — which covers String, Long, Integer, UUID, Date, Instant, LocalDate, BigDecimal, BigInteger, Enum, and anything else the Converter knows how to stringify. The plan-aware call sites (writeObjectFields, writeObjectInline) set a transient currentMapHasSimpleKeyTypeHint flag on the writer before dispatching a Map field value; writeMap consumes the hint at entry and skips the full-key scan when set. Flag is cleared before recursing so nested Maps get their own dispatch. Non-plan paths (Map as root, Map inside a Collection element, etc.) still run hasComplexKeys — correctness preserved. Eliminates ~29 JFR samples (~3% of Map-write path) per benchmark run from hasComplexKeys's iteration alone, with additional savings from skipping the keyset iterator allocation. Measured 3-run medians vs Jackson: Toon Write cycleSupport=true toMaps 2.03x → 1.89x (-7%) — below 2x; cycleSupport=true toJava 1.87x → 1.81x (-3%); cycleSupport=false toJava 1.73x → 1.69x; cycleSupport=false toMaps 1.80x → 1.71x. JSON writes unaffected (different code path).
  • PERFORMANCE: ReadOptions.isNonReferenceableClass and isNotCustomReaderClass are now memoized via per-instance ClassValue<Boolean> caches, mirroring the existing WriteOptions.nonRefCache pattern. The previous implementations re-ran the full chain on every call — a Set.contains, three isAssignableFrom hierarchy walks, an isEnum() check, and an AnnotationResolver.getMetadata(clazz) ClassValueMap lookup — and JFR showed this chain as the top leaf in the Resolver path (~18% of Resolver samples combined). With caching, after the first touch the result is an identity-based ClassValue read. Measured 3-run medians on JsonPerformanceTest Read: JsonIo Read toJava 9211 → 8919 ms (-3.2%, ratio 1.99x → 1.97x — below 2x vs Jackson); Toon Read toJava 9483 → 9051 ms (-4.6%, ratio 2.05x → 2.00x); JsonIo Read toMaps 5670 → 5521 ms (-2.6%); Toon Read toMaps 7429 → 7025 ms (-5.4%). Both JSON and TOON reads benefit because both route through the same ReadOptions.
  • PERFORMANCE: JsonWriter.traceReferences — filter non-referenceable leaves (Strings, primitive wrappers, temporals, UUID, etc.) at the push site instead of after pop. Mirrors the pattern ToonWriter.pushReferenceCandidate has used for years. Map keys/values, Collection elements, and POJO field values that are non-referenceable are counted toward the DOS guardrails (maxObjectCount, maxObjectGraphDepth) but never land on the trace stack — saving a deque push + pop + redundant isNonReferenceable re-check per leaf. A new private traceVisit(Object, Deque, int) helper centralizes the filter and DOS enforcement; processMap, processCollection, processArray, and processFields all route through it. DOS tests unchanged (the count is maintained). cycleSupport=false path is unaffected (traceReferences is skipped entirely when cycles are off). Measured 3-run medians: JsonIo Write cycleSupport=true toJava 5582 → 5256 ms (-5.8%), toMaps 6229 → 5759 ms (-7.5%). No regression on cycleSupport=false or TOON paths.
  • PERFORMANCE: ToonWriter — precompute POJO field-key quoting decision at WriteFieldPlan build time. Previously, needsQuoting(key) ran on every field write and probed a static ConcurrentHashMap<String, Boolean> (the shared quoteDecisionCache). JFR showed ConcurrentHashMap.get from needsQuoting as 11.7% of ToonWriter samples — 72 out of 613. New ToonWriter.computeKeyNeedsQuoting(String, WriteOptions) static helper captures the full decision (empty check, dot-in-key, reserved literals, delimiter-aware character scan) once per (WriteOptions, Class) at plan build time. Runtime writeFieldEntry/writeFieldEntryInline now take a precomputed keyNeedsQuoting boolean and read it directly — no cache probe. Dynamic Map keys (where no plan is available) still route through the existing cache via a small needsQuotingForMapKey helper. Post-optimization JFR: the CHM-get-from-needsQuoting leaf is gone; remaining needsQuoting samples are from Map keys only. Measured 3-run medians: Toon Write cycleSupport=false toJava 4568 → 4341 ms (-5.0%), toMaps 4690 → 4521 ms (-3.6%); cycleSupport=true toJava 5374 → 5206 ms (-3.1%), toMaps 5761 → 5483 ms (-4.8%). Best toJava Toon Write ratio vs Jackson improved from 1.79x to 1.70x (cycleSupport=false).
  • PERFORMANCE: JsonWriter.writeField() — added primitive/String fast path that bypasses writeImpl()'s full dispatch chain for the most common field value types (String, Integer, Long, Boolean, Double). When no @IoShowType, @IoFormat, or forceElementShowType is active, and the value type doesn't require @type (checked via isForceType()), field values are written directly — skipping writeImpl's null/security checks, activePath tracking, writeUsingCustomWriter lookup, @IoValue annotation check, writeTypeCache ClassValue dispatch, and the try/finally container-state save/restore. String is unconditionally fast-pathed (always a native JSON type); numeric/boolean types check isForceType() first. Works in all showTypeInfo modes. Measured 3-run medians: JsonIo Write cycleSupport=false toJava 4432 → 4125 ms (-6.9%), toMaps 4382 → 4111 ms (-6.2%). Best JSON Write toJava ratio vs Jackson improved from 1.68x to 1.61x (cycleSupport=false).
  • PERFORMANCE: ToonWriter.writeFieldEntry() and writeFieldEntryInline() — added primitive/String fast path at the top of each method. For null, String, Integer, Long, Boolean, and Double field values (the vast majority of POJO fields), the value is written directly without falling through the container-type checks (char[], array, Collection, Map) and the isPrimitive() + writeValue() dispatch chain. Each primitive field now requires 1 instanceof check instead of 4+ container misses followed by an isPrimitive() call (which does its own 4 instanceof checks) and a writeValue() call (which re-checks instanceof). Measured 3-run medians: Toon Write cycleSupport=false toJava 4976 → 4663 ms (-6.3%), toMaps 5067 → 4775 ms (-5.8%). Best toJava Toon Write ratio vs Jackson improved from 2.00x to 1.85x (cycleSupport=false).
  • PERFORMANCE: JsonWriter.writeCollectionElement() — added POJO short-circuit for cycleSupport=false that skips writeImpl()'s null/security/primitive checks, activePath tracking, and try/finally overhead. For POJO elements in collections (the common case for List<MyPojo>), the dispatch goes directly from writeCollectionElementwriteObject (2 levels) instead of writeCollectionElementwriteImplwriteTypeCachewriteObject (4 levels), giving the JIT better inlining budget. Custom writers and @IoValue annotations are checked first for correctness. Architecturally cleaner dispatch path for the cycleSupport=false convergence default.
  • PERFORMANCE: ToonReader.parseNumber() — added isValidNumberToken() pre-validation guard before MathUtilities.parseToMinimalNumericType() calls. Tokens that start with a digit but contain characters impossible in a numeric literal (like '-' at non-sign positions, 'T', ':', letters other than 'e'/'E') are rejected immediately without calling the parse method. This eliminates the expensive NumberFormatException + Throwable.fillInStackTrace() path that was triggered for unquoted date/time strings (2024-03-15, 10:30:00Z, etc.) which start with a digit and pass isLikelyNumberStart() but are not valid numbers. JFR profiling showed fillInStackTrace as the #1 leaf in TOON Read (291 samples) — each exception creation costs ~1-5 μs of pure stack-trace construction waste. The guard is a single-pass char scan that is effectively free (early-exits on first invalid char). Applied to both the char[] and String overloads of parseNumber(). Measured 3-run medians: Toon Read toJava 11607 → 9708 ms (-16.4%), Toon Read toMaps 9701 → 7480 ms (-22.9%). Best toJava TOON Read ratio vs Jackson improved from 2.69x to 2.15x and toMaps from 2.39x to 1.84x.
  • PERFORMANCE: ToonReader.parseNumber(char[]) — added a digits-only positive-integer fast path at the top of the method that bypasses the number cache entirely. For tokens matching [0-9]+ that fit in a non-negative long (the dominant shape in int/long fields, ID columns, counters, and int[]/long[] array elements), we parse in place with an inline overflow-guarded accumulator and return Long.valueOf(result) directly — skipping the cacheHash computation, the number-cache charAt miss-check loop, the cacheSubstringFromBuf key materialization (which did a redundant second probe of the string cache), and the number-cache store. The fast path falls through to the existing full path for signed integers, decimals, scientific notation, BigInteger, and anything else. JFR baseline showed ToonReader.parseNumber(char[]) at ~7% of TOON Read phase samples and ToonReader.cacheSubstringFromBuf(char[]) at ~8% (a significant fraction driven by the parseNumber miss-path call). Measured 3-run medians: Toon Read toJava 12634 → 11834 ms (-6.3%), Toon Read toMaps 10424 → 9838 ms (-5.6%). Best toJava TOON Read ratio vs Jackson improved from 2.84x to 2.71x and toMaps from 2.55x to 2.46x.
  • PERFORMANCE: ToonWriter.writeValue() / ToonWriter.writeNumber() — added Integer / Long direct-write fast paths in writeValue (calling toCachedLongString with the SMALL_LONG cache) that skip both the writeNumber method dispatch and its 6-wide instanceof chain (Double/Float/BigDecimal/BigInteger/AtomicInteger/AtomicLong all miss before hitting the common-case Integer/Long branch). writeNumber itself was reordered to check Integer/Long/Short/Byte first, benefitting any path that still reaches it. Measured 3-run medians: Toon Write cycleSupport=true 5808 → 5504 ms (-5.2%, toJava), cycleSupport=false 4905 → 4751 ms (-3.1%, toJava); cycleSupport=true 6242 → 5957 ms (-4.6%, toMaps), cycleSupport=false 5100 → 4877 ms (-4.4%, toMaps). Best toJava Toon Write ratio vs Jackson improved from 1.89x to 1.86x (cycleSupport=false).
  • PERFORMANCE: ToonWriter.writeValue() and ToonWriter.isPrimitive() — added cheap instanceof fast paths for String / Number / Boolean / Character that skip the per-call writeTypeCache.get(value.getClass()) (a ClassValue<ToonWriteType> lookup). JFR profiling of the post-toToon-pipeline baseline showed ClassValue$ClassValueMap.loadFromCache (3.3%) + ClassValue.match (3.3%) + ClassValue.getCacheCarefully (1.2%) ≈ 7-8% of ToonWriter samples, driven by the fact that every field value flows through both isPrimitive() (in writeFieldEntry) and writeValue(), each doing its own ClassValue lookup. The fast paths cover the common primitive-wrapper case with a few cheap instanceof checks before falling through to the ClassValue dispatch for CONVERTER_SUPPORTED / POJO / VALUE_METHOD types. Measured 3-run medians: Toon Write cycleSupport=true 6186 → 5808 ms (-6.1%, toJava), cycleSupport=false 5181 → 4905 ms (-5.3%, toJava); cycleSupport=true 6742 → 6242 ms (-7.4%, toMaps), cycleSupport=false 5337 → 5100 ms (-4.4%, toMaps). Best toJava Toon Write ratio vs Jackson improved from 2.05x to 1.89x (cycleSupport=false).
  • PERFORMANCE: JsonIo.toToon(Object, WriteOptions) — swapped the byte-based output pipeline (FastWriterOutputStreamWriter(UTF-8)FastByteArrayOutputStreamnew String(bytes, UTF-8)) for a direct char-based pipeline (StringBuilderWriterStringBuildersb.toString()), mirroring the earlier JsonIo.toJson fix. JFR profiling of JsonPerformanceTest (TOON Write baseline) showed ~33% of ToonWriter samples in the char→byte→char round-trip: sun.nio.cs.UTF_8$Encoder.encodeArrayLoopSlow (8.1%), StringLatin1.getChars (7.9%), String.getChars (7.1%), Preconditions.checkFromIndexSize (4.3%), FastWriter.write(String, int, int) (~6%). StringBuilder stays in compact Latin-1 storage for pure-ASCII TOON, so the returned String is materialized with a single copy instead of a UTF-8 decode pass. The toToon(OutputStream, ...) overload is unchanged. Measured 3-run medians on the expanded JsonPerformanceTest: Toon Write cycleSupport=true 6562 → 6186 ms (-5.7%, toJava), cycleSupport=false 5545 → 5181 ms (-6.6%, toJava); cycleSupport=true 7192 → 6742 ms (-6.3%, toMaps), cycleSupport=false 5745 → 5337 ms (-7.1%, toMaps). Best toJava Toon Write ratio vs Jackson improved from 2.15x to 2.05x (cycleSupport=false).
  • PERFORMANCE: JsonWriter.writeImpl() — added a primitive-wrapper fast path for Integer / Long / Boolean when !showType && !forceElementShowType && fieldFormatPattern == null. Previously these routed through writeUsingCustomWriterwriteCustomgetCustomWriterIfAllowedPrimitiveValueWriter.writePrimitiveForm, which calls obj.toString() (allocating a String) and then output.write(String). The fast path now calls writeIntDirect / writeLongDirect (which stream digits via pre-computed digit-pair tables straight to the output buffer with no String allocation) or the cached SMALL_INT_STRINGS entry for ints in [-128, 16384], and writes "true" / "false" as a direct literal for Boolean. It also bypasses the activePath tracking setup, the writeUsingCustomWriter dispatch, and the CustomWriterGate lookup entirely for these leaf types. Measured 3-run medians: cycleSupport=true toJava 6215 → 5708 ms (-8.2%), cycleSupport=false toJava 4713 → 4310 ms (-8.5%); cycleSupport=true toMaps 6918 → 6368 ms (-8.0%), cycleSupport=false toMaps 4779 → 4263 ms (-10.8%). Best toJava write ratio vs Jackson improved from 1.84x to 1.68x (cycleSupport=false).
  • PERFORMANCE: JsonIo.toJson(Object, WriteOptions) — swapped the byte-based output pipeline (FastWriterOutputStreamWriter(UTF-8)FastByteArrayOutputStreamnew String(bytes, UTF-8)) for a direct char-based pipeline (StringBuilderWriterStringBuildersb.toString()). JFR profiling of JsonPerformanceTest showed roughly half of JSON write time was spent inside sun.nio.cs.UTF_8$Encoder.encodeArrayLoopSlow (15.5%), StringLatin1.getChars (13.5%), jdk.internal.util.Preconditions.checkFromIndexSize (9.1%), and String.getChars (8.8%) — all driven by the char→byte→char round-trip that toJson(String) does not need. StringBuilder stays in compact Latin-1 storage for pure-ASCII JSON, so the returned String is materialized with a single copy instead of a UTF-8 decode pass. The toJson(OutputStream, ...) overload is unchanged. New StringBuilderWriter helper class. Measured 3-run medians on the expanded JsonPerformanceTest: JsonIo Write cycleSupport=true 6676 → 6215 ms (-6.9%, toJava), cycleSupport=false 5257 → 4713 ms (-10.3%, toJava); cycleSupport=true 7551 → 6918 ms (-8.4%, toMaps), cycleSupport=false 5262 → 4779 ms (-9.2%, toMaps). Best toJava write ratio vs Jackson improved from 2.05x to 1.84x (cycleSupport=false).
  • PERFORMANCE: Injector pre-computes a fastPath byte tag at plan-build time (PRIMITIVE_NUMERIC / BOOLEAN / STRING / GENERIC). ObjectResolver.assignField() checks this tag first and short-circuits primitive numeric / boolean / String assignments before traversing the 5-case dispatch chain (null → reference → scalar → array → jsonObject). For POJO-heavy workloads, this eliminates ~5 per-field branch checks and ~3% of toJava read time. Best toJava ratio on the expanded test: 1.95x (down from 2.01x) vs Jackson.
  • PERFORMANCE: ReadOptionsBuilder.buildInjectors() — switched the cached injector map from LinkedHashMap to HashMap. Nothing in the read/parse path iterates this map (all callers use .get(fieldName)), so insertion-order tracking is pure overhead. MapResolver.traverseFields — which hits injectorMap.get(fieldName) per field — improved ~2.5% on toMaps reads.
  • PERFORMANCE: JsonValue.setType() — added fast path for Class types. Class types never contain unresolved type variables (no generics), so the per-call ConcurrentHashMap.computeIfAbsent() validation cache lookup is pure overhead for the 95%+ common case. A single instanceof Class check eliminates the CHM operation. Read times improved ~2.6% (toJava) and ~3.4% (toMaps).
  • PERFORMANCE: JsonParser.readJsonObject() — skip substitutes.getOrDefault() HashMap lookup for field names that don't start with @ or $. The SUBSTITUTES map only holds short-form meta keys (@i/@t/@r/@e/@k) and JSON5 $-prefixed meta keys ($type/$id/etc.). Regular field names (letters, digits) never match, so the HashMap lookup was pure overhead for the 99%+ common case. A single charAt(0) check eliminates it. Read ratio improved from 2.22x to 2.14x for toJava and 1.31x to 1.26x for toMaps (vs Jackson).
  • PERFORMANCE: JsonParser.readString() — added fast path for the common case of short strings (< 256 chars) without escape sequences. Bypasses StringBuilder entirely: bulk-reads into a reusable char[] via readUntil(), then caches directly from the char array via new cacheStringFromChars() method. Eliminates StringBuilder.setLength(0) + append() + toString() overhead for every non-escaped string. Escape sequences and long strings fall through to the original StringBuilder-based slow path. Read ratio (JsonIo/Jackson) improved from 2.37x to 2.25x for toJava and from 1.48x to 1.35x for toMaps.
  • PERFORMANCE: JsonParser.readNumber() — added fast path for simple positive integers (1-9 followed by digits). Accumulates directly into a long from the stream — no StringBuilder, no numBuf.append() per digit, no readInteger() re-parse. Falls back to StringBuilder path for negative numbers, floats, hex, or 19+ digit integers. Combined with readString optimization, toJava read ratio improved from 2.33x to 2.17x vs Jackson.
  • PERFORMANCE: Resolver.DefaultReferenceTrackerHashMap for @id/@ref tracking is now allocated lazily on first put() call. Most JSON has no object references, so the HashMap is never created, eliminating per-parse allocation overhead.
  • REFACTOR: ToonReader.readInlineObject now parses the first field directly from lineBuf coordinates instead of materializing a String firstFieldLine at the caller and re-scanning it. The caller (readListArray) pre-checks the hyphen-line slice with buffer-based findColonInBuf(start, end) and lineBuf[start] != '"' rather than allocating elementContent as a String. Eliminates the per-element String allocation, a redundant findColonPosition scan, and two trimAsciiRange substring allocations per inline object in a list. Consolidates two parseCombinedArrayField overloads into one (the String-based overload had no other callers) and removes the now-unused findColonPosition(String) helper. Behavior is unchanged; all 3787 tests pass. Measured perf impact on JsonPerformanceTest was within run-to-run noise because the test data favors tabular-array reads (via readTabularArray) over inline-object reads — this code path is cold in the benchmark. The refactor is shipped for code hygiene (fewer overloads, uniform char[]-based parsing throughout ToonReader); real-world workloads with non-uniform list elements should see allocation reductions.
  • NOTE: One-time deprecation warning logged (via java.util.logging) when a Map with stringify-able keys (Long, UUID, etc.) is written using @keys/@items format because stringifyMapKeys is off. The warning fires at most once per JVM lifetime (guarded by AtomicBoolean) and recommends stringifyMapKeys(true) or standardJson(). Prepares users for the default change in json-io 5.0.
  • NOTE: JsonObject internal storage refactored — eliminated the values[], items[], and effectiveValues pointer (three Object[] fields) in favor of two fields: data[] (parallel values array for POJO/Map entries, grown by put()/appendFieldForParser()) and itemsRef (externally-provided @items array, set by setItems()). The fragile effectiveValues pointer — which had to be kept in sync across setItems(), setKeys(), and ensureCapacity() — is replaced by a trivially-inlined valueArray() method that selects the correct array based on storageMode. Public API is unchanged (getItems(), getKeys(), setItems(), setKeys(), Map interface all behave identically). Per-instance memory reduced by one Object[] reference (4 bytes on compressed oops). Performance verified: no regression across JsonIo Read, Toon Read, or Write benchmarks. Added 9 new JUnit tests for JsonObject covering the refactored storage paths (constructor edge cases, EntrySet/ValuesCollection views, rehashMaps() key/value unwrapping, items-mode hashCode/equals, and asTwoArrays() error handling).
  • BUILD: Added JaCoCo code coverage plugin (jacoco-maven-plugin 0.8.12) permanently. prepare-agent and report goals wired in; surefire argLine updated to @{argLine} for agent injection. Coverage reports are generated automatically on every mvn test run at target/site/jacoco/. No impact on Maven Central deployment or build artifacts.
  • DEPENDENCY: Requires java-util 4.101.0 which ships a major internal Converter refactor that benefits all json-io reads and writes. Converter's four internal caches (CONVERSION_DB, FULL_CONVERSION_CACHE, USER_DB, cacheInheritancePairs) are now a nested ClassValueMap<ClassValueMap<V>> keyed by (source class, target class). This eliminates all per-lookup ConversionPair allocation from json-io's read-side type-conversion hot path, and leverages ClassValueMap's identity-based fast path at both tiers. JFR previously showed ConversionPair as a top-5 allocation class during json-io reads of diverse data (UUID, Date, Instant, BigDecimal) — it is now eliminated from the hot path.
  • TESTING: Added StringKeyConversionTest — 13 tests verifying that json-io correctly deserializes standard JSON with String map keys (e.g., {"100": "value"}) into typed Map<K, V> instances where the key type is Long, Integer, Double, Boolean, BigDecimal, BigInteger, UUID, Date, ZonedDateTime, AtomicLong, or Character. Also verifies backward compatibility with the old @keys/@items parallel-array format and empty map handling. This capability (introduced in 4.94.0 via the mapKeyType pipeline) was previously untested.

... (truncated)

Commits
  • 2270889 Performance: switch nonRef / notCustom membership tests to ClassValueSet.cont...
  • 63ef1f2 Performance: switch ClassValueMap hot-path lookups to new getByClass(Class) f...
  • 38b4d07 Revert "Performance: bulk-copy strings to local char[] in JSON/TOON writer sc...
  • a8d1f76 Performance: bulk-copy strings to local char[] in JSON/TOON writer scan loops
  • 8222c9c Docs: finalize 4.101.0 changelog — set release date, add AnnotationResolver s...
  • a5ab1b5 Performance: split AnnotationResolver.getMetadata into fast-path + cold-path ...
  • 49a6a58 Docs: update README performance numbers to match 4.101.0 measured ratios
  • 5f05e1a Performance: hoist AnnotationResolver.getMetadata once per object in ObjectRe...
  • ac38bda Feature: Global namingStrategy WriteOption for Jackson-compat migration
  • 53df7f0 Docs: tighten the Jackson-compat Key Features bullet
  • Additional commits viewable in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [com.cedarsoftware:json-io](https://github.com/jdereg/json-io) from 4.93.0 to 4.101.0.
- [Release notes](https://github.com/jdereg/json-io/releases)
- [Changelog](https://github.com/jdereg/json-io/blob/master/changelog.md)
- [Commits](jdereg/json-io@4.93.0...4.101.0)

---
updated-dependencies:
- dependency-name: com.cedarsoftware:json-io
  dependency-version: 4.101.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file java Pull requests that update java code labels Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file java Pull requests that update java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants