| Artifact | io.github.microsphere-projects:microsphere-java-core (0.3.20-SNAPSHOT, release branch) |
| Package | io.microsphere.io.serializer — 18 main-source types |
| JDK level | Compiled at Java 8; CI-tested on JDK 8 / 11 / 17 / 21 / 25 |
Depends on other core packages |
io.microsphere.reflect (JavaType, MethodUtils.invokeStaticMethod, TypeUtils.resolveTypeArgumentClasses), io.microsphere.io (FastByteArrayInputStream, FastByteArrayOutputStream, IOUtils), io.microsphere.util (SizeUtils, ClassLoaderUtils, ServiceLoaderUtils, ArrayUtils), io.microsphere.collection (ListUtils, MapUtils), io.microsphere.annotation.Nonnull |
| External dependencies | none — pure JDK |
| Used by | convert — ObjectToByteArrayConverter and ByteArrayToObjectConverter call the DefaultSerializer / DefaultDeserializer singletons. The dependency runs convert → io.serializer, never the reverse. |
All file:line references in this page are relative to microsphere-java-core/src/main/java/io/microsphere/
(main sources) or microsphere-java-core/src/test/java/io/microsphere/ (tests), read from the release worktree.
After the first mention they are shortened to io/serializer/AbstractSerializer.java:40-55.
Reach for io.microsphere.io.serializer when you need to turn one of a small, fixed set of value types into a
byte[] and back with a layout you can predict. The package ships a Serializer / Deserializer SPI and nine
ready-made implementations for boolean, byte, char, short, int, long, float, double and String.
Each numeric one is a pure bit-layout codec with no allocation beyond the result array — for example
IntegerSerializer writes four big-endian bytes by hand (io/serializer/IntegerSerializer.java:21-30) and
BooleanSerializer writes one byte 1/0 (io/serializer/BooleanSerializer.java:26-30). If your goal is a stable
compact binary encoding of a scalar, these are the right tool.
Reach for Serializers / Deserializers when the concrete type is only known at runtime — a cache key encoder,
a generic record store, a wire protocol that tags fields by Java type. They are per-ClassLoader registries that
load every Serializer registered through META-INF/services and let you ask for the best match by type
(io/serializer/Serializers.java:87-129).
Reach for the AbstractSerializer<T> base class when you are adding a codec. It already handles null, already
validates the fixed byte width, and already discovers the type you encode, so a new codec is three small methods
(io/serializer/AbstractSerializer.java:70-137).
Do not reach for it when:
- You need a real object graph. The only general-purpose implementation,
DefaultSerializer, is a thin wrapper overjava.io.ObjectOutputStream(io/serializer/DefaultSerializer.java:52-62). It is JDK serialization — with the JDK's stream format, the JDK'sserialVersionUIDrules, and the JDK's deserialization attack surface. Use a schema-based format (Protobuf, Avro, JSON) instead. - You need untrusted input handling.
DefaultDeserializercallsObjectInputStream.readObject()with no class filter of any kind. See §6 — this is the single most important caveat on the page. - You need a general text encoding.
StringSerializer/StringDeserializerare convenience codecs, not a charset framework; they are not registered in a way that lets you round-trip anull. - You need
Enumencoding via the registry.EnumSerializerexists and works, but it is not in the SPI files, soSerializerswill never hand it to you. See §5.
<dependency>
<groupId>io.github.microsphere-projects</groupId>
<artifactId>microsphere-java-core</artifactId>
<version>0.3.20</version> <!-- newest published tag; 0.3.20-SNAPSHOT is this same line's working version -->
</dependency>| Fact | Value | Evidence (release) |
|---|---|---|
groupId |
io.github.microsphere-projects |
pom.xml:12 |
artifactId |
microsphere-java-core |
module pom.xml |
| Version property | ${revision}, flattened |
pom.xml:14 |
Value of ${revision} on release |
0.3.20-SNAPSHOT |
pom.xml:54 |
| Newest published tag | 0.3.20 |
release-line table in the group index (docs/current/user-guide/microsphere-java-core/README.md:70) |
| Build parent | microsphere-build:0.3.16 |
pom.xml:6-10 |
| Java level | 8, inherited from the external build parent; no maven.compiler.* / <release> property in the reactor |
CI matrix java: [ '8', '11', '17', '21', '25' ], .github/workflows/maven-build.yml:25, distribution: 'temurin' :33, build command :38-45 |
| Spring Framework, JDK 17+ | 7.0.9 |
microsphere-java-parent/pom.xml:23 |
Spring Framework, JDK [1.8,17) |
5.3.39 via profile java8-16 |
microsphere-java-parent/pom.xml:54-62 |
| Imported BOMs | see microsphere-java-parent/pom.xml:22, :33-36, :40-46 |
same |
@Since of every type in this package |
1.0.0 |
io/serializer/Serializer.java:38, AbstractSerializer.java:52, EnumSerializer.java:15, DefaultDeserializer.java:49 |
Tip
Nothing in this package touches Spring, so the java8-16 profile is irrelevant here. What is JDK-sensitive is the
DefaultSerializer / DefaultDeserializer pair: their output is the JDK serialization stream, whose behaviour and
default filter configuration changed across JDK 8 → 25. See §5.
| Item | Value | Evidence |
|---|---|---|
| Documented line | release, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Newest published release | 0.3.20, tagged 2026-10-16 |
the 0.3.20 tag resolves to chore: merge main into release [skip ci], committed 2026-10-16T03:43:20Z |
| Previous release | 0.3.19, tagged 2026-08-30 |
the 0.3.19 tag, committed 2026-08-30T12:51:37Z |
| Changelog source | release-notes.md at the repository root |
The project publishes no CHANGELOG.md; the v0.3.16–v0.3.20 blocks of release-notes.md read Release notes generation failed and carry raw commit lists, so every entry below is quoted as a commit subject and checked against the release sources. |
0.3.20 adds no new API surface for this area — the 0.3.19...0.3.20 range is a hardening release. What it does change, and how far that reaches the sources this page documents:
- Nullability contracts. 3 of 18 main sources in the scope of this page declare
@Nonnull/@Nullable. Subjects in the range that name this area:30507ecd Annotate serializer APIs with nullability. - Null-input and behaviour fixes.
0da2976b Inline return in Character deserializer/93147704 Inline BooleanSerializer byte array return.
Note
Reading the commit list: 93147704 Inline BooleanSerializer byte array return names a type that exists on this line (io.microsphere.io.serializer.BooleanSerializer). 0da2976b Inline return in Character deserializer does not: there is no CharacterDeserializer here, the closest types being CharacterSerializer and DefaultDeserializer, so that subject is quoted but not attributed to a file. Neither commit touches the io.microsphere.json writers.
Serializer<S> — one method, not a functional interface (io/serializer/Serializer.java:40-42):
public interface Serializer<S> {
byte[] serialize(S source) throws IOException;
}Deserializer<T> — a @FunctionalInterface, so a lambda is a legal implementation (io/serializer/Deserializer.java:40-43):
@FunctionalInterface
public interface Deserializer<T> {
T deserialize(byte[] bytes) throws IOException;
}Both contracts require implementations to be thread-safe (io/serializer/Serializer.java:24-25,
io/serializer/Deserializer.java:24). Every built-in implementation is stateless-after-construction, so the
public static singletons are safe to share.
| Entry point | Signature | Notes |
|---|---|---|
new Serializers() / new Serializers(ClassLoader) |
Serializers() (io/serializer/Serializers.java:68-70), Serializers(ClassLoader) (:64-66) |
the no-arg form uses ClassLoaderUtils.getDefaultClassLoader() |
Serializers#loadSPI |
void loadSPI() (:72-79) |
must be called before any lookup; not @Nonnull-guarded, idempotent only by accident — see §6 |
Serializers#getMostCompatible |
Serializer<?> getMostCompatible(Class<?> serializedType) (:87-93) |
falls back to the Object.class entry; javadoc says "null if not found" (:85) but the method carries no nullability annotation |
Serializers#getHighestPriority |
<S> Serializer<S> getHighestPriority(Class<S> serializedType) (:102-105) |
first(list); null on empty, javadoc-documented only (:100) |
Serializers#getLowestPriority |
<S> Serializer<S> getLowestPriority(Class<S> serializedType) (:114-117) |
last(list); null on empty, javadoc-documented only (:112) |
Serializers#get |
@Nonnull <S> List<Serializer<S>> get(Class<S> serializedType) (:126-129) |
the only annotated accessor in the class; returns emptyList(), never null |
Deserializers |
same five shapes (io/serializer/Deserializers.java:64-129) |
see §5 for the caching asymmetry |
AbstractSerializer singletons |
BOOLEAN_SERIALIZER, BYTE_SERIALIZER, CHARACTER_SERIALIZER, SHORT_SERIALIZER, INTEGER_SERIALIZER, LONG_SERIALIZER, FLOAT_SERIALIZER, DOUBLE_SERIALIZER |
e.g. io/serializer/IntegerSerializer.java:13 |
DefaultSerializer.DEFAULT_SERIALIZER |
public static final DefaultSerializer (io/serializer/DefaultSerializer.java:49) |
JDK-stream serializer for Object |
DefaultDeserializer.DEFAULT_DESERIALIZER |
public static final DefaultDeserializer (io/serializer/DefaultDeserializer.java:53) |
JDK-stream deserializer for Object |
new EnumSerializer<>(Class<E>) |
io/serializer/EnumSerializer.java:27-31 |
per-enum instance; no SPI registration |
| Type | Implementing class | Width | calcBytesLength() evidence |
Byte order |
|---|---|---|---|---|
Boolean |
BooleanSerializer |
1 | io/serializer/BooleanSerializer.java:21-23 (BOOLEAN_BYTES_SIZE) |
n/a |
Byte |
ByteSerializer |
1 | io/serializer/ByteSerializer.java:36-38 |
n/a |
Character |
CharacterSerializer |
2 | io/serializer/CharacterSerializer.java:34-36 |
big-endian (:39-45) |
Short |
ShortSerializer |
2 | io/serializer/ShortSerializer.java:16-18 |
big-endian (:21-27) |
Integer |
IntegerSerializer |
4 | io/serializer/IntegerSerializer.java:16-18 |
big-endian (:21-30) |
Float |
FloatSerializer |
4 | io/serializer/FloatSerializer.java:36-38 |
big-endian, via INTEGER_SERIALIZER (:41-44) |
Long |
LongSerializer |
8 | io/serializer/LongSerializer.java:16-18 |
little-endian (:21-34) |
Double |
DoubleSerializer |
8 | io/serializer/DoubleSerializer.java:21-23 |
little-endian, via LONG_SERIALIZER (:26-30) |
String |
StringSerializer / StringDeserializer |
variable | charset-encoded; StringSerializer.java:57-59, StringDeserializer.java:69-71 |
n/a |
Enum<E> |
EnumSerializer |
1 or 2 | io/serializer/EnumSerializer.java:33-36 — 1 while the constant count is below Byte.MAX_VALUE (128), else 2 |
big-endian for the 2-byte case (:56, :69) |
Object (JDK stream) |
DefaultSerializer / DefaultDeserializer |
variable | io/serializer/DefaultSerializer.java:52-62 |
JDK stream format |
Widths come from SizeUtils: UNBOUND_BYTES_SIZE = -1, BOOLEAN/BYTE = 1, SHORT/CHAR = 2,
INTEGER/FLOAT = 4, LONG/DOUBLE = 8 (io/util/SizeUtils.java:38-78).
Warning
The 2/4/8-byte families do not share a byte order. Integer/Float are big-endian, Long/Double are
little-endian. This is visible directly in the source: IntegerSerializer.java:24 puts the most-significant byte
at index 0, while LongSerializer.java:24 puts the least-significant byte there. Any cross-type binary format
you build on these must handle the switch explicitly. See §6.
You never see a stream. Both DefaultSerializer and DefaultDeserializer open their FastByteArray* and
Object*Stream objects inside a try-with-resources block scoped to the single method and close them before
returning (io/serializer/DefaultSerializer.java:54-60, io/serializer/DefaultDeserializer.java:61-68).
- The buffers are
io.microsphere.io.FastByteArrayOutputStream/FastByteArrayInputStream, and both have no-opclose()(io/io/FastByteArrayOutputStream.java:188-189,io/io/FastByteArrayInputStream.java:157-158). Theclose()call can never mask or suppress your real exception. FastByteArrayOutputStream.toByteArray()returnsArrays.copyOf(buf, count)— a fresh array (io/io/FastByteArrayOutputStream.java:159-161). Thebyte[]you get back is therefore yours: mutating it, or holding it after the serializer is GC'd, cannot corrupt anything.FastByteArrayInputStreamis constructed over the caller's array without copying it (io/io/FastByteArrayInputStream.java:81-83, javadoc at:73"The buffer array is not copied"). Since the stream dies insidedeserialize, this only means: do not mutate the input array while adeserializecall is in flight on another thread.
This is where the package is deliberately inconsistent, and it matters more than the layouts:
| Call | Result | Evidence |
|---|---|---|
AbstractSerializer.serialize(null) |
null |
io/serializer/AbstractSerializer.java:71-77 |
AbstractSerializer.deserialize(null) |
null |
io/serializer/AbstractSerializer.java:80-84 |
AbstractSerializer.deserialize(wrongWidth) |
null — silently, no exception |
io/serializer/AbstractSerializer.java:87-89 |
EnumSerializer.serialize(null) / deserialize(null) |
null |
io/serializer/EnumSerializer.java:44-47, :64-67 |
DefaultDeserializer.deserialize(null or empty) |
null |
io/serializer/DefaultDeserializer.java:57-59 |
StringSerializer.serialize(null) |
NullPointerException |
io/serializer/StringSerializer.java:57-59 — source.getBytes(charset) with no guard |
StringDeserializer.deserialize(null) |
NullPointerException |
io/serializer/StringDeserializer.java:69-71 — new String(null, charset) |
StringDeserializer.deserialize(new byte[0]) |
"" |
asserted in StringSerializerAndDeserializerTest.java:60-64 |
The wrongWidth → null rule is a degradation to a neutral value, in line with the library-wide policy: a
mis-sized array never throws, it just gives you null. Which means the caller must check.
Every snippet below is condensed from an actual test body in
microsphere-java-core/src/test/java/io/microsphere/io/serializer/.
SerializersAndDeserializersTest.java:40-46 — the two registries are constructed and loaded separately:
serializers = new Serializers();
deserializers = new Deserializers();
serializers.loadSPI();
deserializers.loadSPI();SerializersAndDeserializersTest.java:49-56 — String resolves to the charset codecs, not the JDK stream:
Serializer<String> serializer = serializers.getHighestPriority(String.class);
String value = "Test";
byte[] bytes = serializer.serialize(value);
assertArrayEquals(value.getBytes(StandardCharsets.UTF_8), bytes); // 4 bytes, not a JDK header
Deserializer<String> deserializer = deserializers.getHighestPriority(String.class);
assertEquals(value, deserializer.deserialize(bytes));SerializersAndDeserializersTest.java:59-65 — getMostCompatible returns a raw-type serializer, so the call site
is untyped and the cast is deferred to you:
Serializer serializer = serializers.getMostCompatible(Integer.class);
Integer value = 1;
byte[] bytes = serializer.serialize(value);
Deserializer deserializer = deserializers.getMostCompatible(Integer.class);
assertEquals(value, deserializer.deserialize(bytes));SerializersTest.java:56-67 and DeserializersTest.java:44-53 both show the two-phase contract. Before
loadSPI(), get(...) is an empty list and every single-value lookup is null; afterwards all are non-null:
// SerializersTest.java:58-66
for (Class<?> supportedClass : SUPPORTED_CLASSES) {
assertEquals(emptyList(), this.serializers.get(supportedClass)); // before loadSPI()
}
this.serializers.loadSPI();
for (Class<?> supportedClass : SUPPORTED_CLASSES) {
assertNotNull(this.serializers.get(supportedClass)); // after loadSPI()
}The set SerializersTest.java:37-47 treats as supported is the registry's whole contract surface —
Boolean, Character, Short, Integer, Long, Float, Double, String, and Object:
private static final Class<?>[] SUPPORTED_CLASSES = {
Boolean.class, Character.class, Short.class, Integer.class, Long.class,
Float.class, Double.class, String.class, Object.class
};Byte and Enum are absent — Byte is in the SPI file but untested here, Enum is not in the file at all.
IntegerSerializerTest.java:31-41 — subclasses of the shared test base just name a singleton and a value; the base
class AbstractSerializerTest.java:33-51 does the round trip, asserts calcBytesLength() == getBytesLength(), and
asserts getTargetType() resolved to the expected class:
class IntegerSerializerTest extends AbstractSerializerTest<Integer> {
@Override // IntegerSerializerTest.java:33-36
protected AbstractSerializer<Integer> getSerializer() {
return INTEGER_SERIALIZER;
}
@Override // IntegerSerializerTest.java:38-41
protected Integer getValue() {
return MAX_VALUE; // static import of Integer.MAX_VALUE
}
}AbstractSerializerTest.java:28-31,59-63 — the same base class runs a null value through every codec, which
is how the null → null contract above is enforced:
@Test
void testNull() throws IOException {
test(this::getNullValue); // getNullValue() returns null by default (AbstractSerializerTest.java:61-63)
}StringSerializerAndDeserializerTest.java:48-57 — pass a Charset to both halves; nothing else changes:
Charset charset = StandardCharsets.ISO_8859_1;
StringSerializer customSerializer = new StringSerializer(charset);
StringDeserializer customDeserializer = new StringDeserializer(charset);
String value = "Test";
byte[] bytes = customSerializer.serialize(value);
assertArrayEquals(value.getBytes(charset), bytes);
assertEquals(value, customDeserializer.deserialize(bytes));StringSerializerAndDeserializerTest.java:66-73 — UTF-16 round-trips too, but only because the same Charset
object was used on both sides:
Charset charset = StandardCharsets.UTF_16;
assertEquals(value, new StringDeserializer(charset).deserialize(new StringSerializer(charset).serialize(value)));DefaultSerializerAndDeserializerTest.java:37-46 — note the source object is typed Object and holds a String
first, then an Integer. Both survive, because the JDK stream carries the class descriptor:
Object value = "Test";
byte[] bytes = serializer.serialize(value);
assertEquals(value, deserializer.deserialize(bytes));
value = 1;
bytes = serializer.serialize(value);
assertEquals(value, deserializer.deserialize(bytes));DefaultDeserializerTest.java:18-23 — the empty/null-input degradation, asserted directly:
private DefaultDeserializer deserializer = DefaultDeserializer.DEFAULT_DESERIALIZER;
@Test
void testDeserializeOnNull() throws IOException {
assertNull(deserializer.deserialize(null));
}EnumSerializerTest.java:41-56 — TimeUnit has 8 constants, well under 128, so one byte carries the ordinal(),
and both null directions return null:
EnumSerializer<TimeUnit> serializer = new EnumSerializer<>(TimeUnit.class);
assertEquals(1, serializer.getBytesLength());
byte[] bytes = serializer.serialize(timeUnit);
assertEquals(timeUnit, serializer.deserialize(bytes));
assertNull(serializer.serialize(null));
assertNull(serializer.deserialize(null));EnumSerializerTest.java:59-69,72-93 — the nested E enum declares exactly 200 constants, pushing the width to 2:
EnumSerializer<E> serializer = new EnumSerializer<>(E.class);
assertEquals(2, serializer.getBytesLength()); // 200 constants > Byte.MAX_VALUE (127)Condensed from the javadoc example at io/serializer/AbstractSerializer.java:22-48, which is the shape the nine
built-in codecs follow. Three protected methods are all you implement:
public class IntegerSerializer extends AbstractSerializer<Integer> { // AbstractSerializer.java:24
public static final IntegerSerializer INTEGER_SERIALIZER = new IntegerSerializer();
@Override protected int calcBytesLength() { return INTEGER_BYTES_SIZE; } // :29-31
@Override protected byte[] doSerialize(Integer v) { /* encode 4 bytes */ } // :34-38
@Override protected Integer doDeserialize(byte[] b) { /* decode 4 bytes */ } // :41-43
}Returning a fixed size from calcBytesLength() opts you into the width guard at
io/serializer/AbstractSerializer.java:87-89; variable-length codecs return UNBOUND_BYTES_SIZE (-1)
instead, which disables it. See Developer Guide — Serialization
§6.1 for the SPI registration half of the job.
DefaultSerializer and DefaultDeserializer add no format of their own — they call
new ObjectOutputStream(...).writeObject(source) (io/serializer/DefaultSerializer.java:54-59) and
new ObjectInputStream(...).readObject() (io/serializer/DefaultDeserializer.java:61-65). Consequences:
- Byte output is the JDK stream format (
0xAC 0xEDmagic + version + class descriptors). It is stable across the JDKs in the CI matrix only because it is the JDK's own stable format; it is not stable against aserialVersionUIDchange or a class-path difference in the peer JVM. serializerequires the argument beSerializable. The code does not check: it delegates toObjectOutputStream, so a non-serializable argument surfaces asjava.io.NotSerializableException, anIOException(io/serializer/DefaultSerializer.java:52declaresthrows IOExceptionand adds no catch).DefaultDeserializerbehaves differently: itscatch (Exception e) { throw new IOException(e); }(io/serializer/DefaultDeserializer.java:66-68) collapses every exception —ClassNotFoundException,InvalidClassException, even aRuntimeExceptionthrown from a target'sreadObject— into a singleIOException. If you need to distinguish them, catchIOExceptionand inspectgetCause().- Post-Java-8 deserialization hardening (
ObjectInputFilter, thejdk.io.objects.*globals) is a JDK feature, not a library feature. §6 states exactly what is and is not reachable here.
There is none in this package. A grep across all 18 sources for serialVersionUID returns nothing. The
serializable marker interfaces that DefaultSerializer/DefaultDeserializer reference are imported only for javadoc
@see links (io/serializer/DefaultSerializer.java:23, io/serializer/DefaultDeserializer.java:23). Practical
effect: if you serialize a class with DefaultSerializer and later change that class's shape without declaring a
serialVersionUID, the JDK recomputes it and DefaultDeserializer throws InvalidClassException — surfaced as an
IOException per §5.1. Pinning a serialVersionUID is entirely your responsibility.
This is easy to trip over:
- Serializer SPI file registers
StringSerializerat line 9; the Deserializer SPI file registersStringDeserializerat line 9. Asymmetric class names, symmetric behaviour — aStringSerializeris registered as aSerializeronly, and vice versa, because each implements only one interface (io/serializer/StringSerializer.java:44,io/serializer/StringDeserializer.java:56). Serializers.loadSPI()requests a cached service load (io/serializer/Serializers.java:73, third argumenttrue);Deserializers.loadSPI()does not (io/serializer/Deserializers.java:73, two-argument overload). The caching default isSERVICE_LOADER_CACHED, parsed from the system propertymicrosphere.service-loader.cachedandfalseby default (io/util/ServiceLoaderUtils.java:107,:112,:138,:410-412). CallingDeserializers.loadSPI()twice therefore re-reads and re-instantiates the providers, and — becauseloadSPIappends to the per-type list (io/serializer/Deserializers.java:76-77) — the second call doubles every registered list. CallingSerializers.loadSPI()twice is safe only as long as the cache is enabled.- Sorting is applied identically in both cases, so this is purely a caching/repetition difference.
ServiceLoaderUtils.loadServicesAsListalways callssort(serviceList, Prioritized.COMPARATOR)(io/util/ServiceLoaderUtils.java:890). None of the 18 types implementsPrioritized, so the comparator returns0for every pair (io/lang/Prioritized.java:71-75) andCollections.sortbeing stable, the SPI file's declaration order is preserved. First match is therefore file order, not a computed priority.
getMostCompatible has a two-step rule (io/serializer/Serializers.java:87-93): try
getHighestPriority(serializedType) (:88), and on null return getLowestPriority(Object.class) (:90).
Because DefaultSerializer implements Serializer<Object> (io/serializer/DefaultSerializer.java:47) and the registry
keys each provider by its first resolved type argument (io/serializer/Serializers.java:74-75), Object.class is
populated with DefaultSerializer — the last entry in the SPI file. So getMostCompatible never returns null
after a successful loadSPI(), for any type. Ask for getMostCompatible(SomeDomainObject.class) and you get
DefaultSerializer silently. getLowestPriority(Object.class) returns the last Object-keyed entry, which the
file order makes DefaultSerializer / DefaultDeserializer. Use getHighestPriority and handle null instead
whenever you need "only the exact codec, or nothing".
[!SECURITY]
DefaultDeserializerperforms unfiltered JDK deserialization. Do not point it at data you do not trust.Verified from the source, on the
releasebranch:
DefaultDeserializer.deserializebuilds a plainjava.io.ObjectInputStreamover the caller's bytes and callsreadObject()—io/serializer/DefaultDeserializer.java:61-65. There is noObjectInputFilter, no subclassedresolveClass, no allow-list, no size cap, and noLatestClock/depth configuration. A grep across all 18 types inio/serializer/forObjectInputFilter,resolveClassandBadPaddingExceptionreturns no matches.- Consequently it does not guard against: gadget-chain instantiation, arbitrary class loading during deserialization,
readObject/readResolveside effects in target classes, deserialization bombs (deep or wide object graphs exhausting heap), orserialVersionUID-based type confusion. It only guards against empty input (:57-59).- The risk is reachable through public API beyond this package:
ByteArrayToObjectConvertercallsDEFAULT_DESERIALIZER.deserialize(...)(io/convert/ByteArrayToObjectConverter.java:22,41) and that converter is registered inMETA-INF/services/io.microsphere.convert.Converter(line 3), so anyConverters-drivenbyte[] → Objectconversion routes untrusted bytes here.Mitigation is yours to add, using the JDK's own mechanism. As of Java 9 (backported to 8u121+) the platform provides
java.io.ObjectInputFilter, settable per-stream viaObjectInputStream.setObjectFilter(...)or globally via thejdk.io.objects.readData/jdk.serialFiltersystem properties. This library'sObjectInputStreamis created insideDefaultDeserializerand is not exposed, so per-stream filtering is not possible throughDefaultDeserializer— the only configuration it will honour is the globaljdk.serialFilterproperty. For anything genuinely untrusted, either set that global filter or replace theObject-keyed SPI entry with your own filteredDeserializer. The package gives you no other lever, and this page does not invent one.
Other real limits:
Long/Doubleare little-endian, everything else is big-endian.IntegerSerializer.java:23-28emits MSB-first;LongSerializer.java:23-32emits LSB-first.FloatSerializerinherits big-endian by delegating toINTEGER_SERIALIZER(io/serializer/FloatSerializer.java:41-44), andDoubleSerializerinherits little-endian by delegating toLONG_SERIALIZER(io/serializer/DoubleSerializer.java:26-30). So a 4-byte field and an 8-byte field in the same record use opposite orders.CharacterSerializer.java:41-44andShortSerializer.java:23-25match the big-endian family.- A wrong-width
deserializereturnsnull, not an error.io/serializer/AbstractSerializer.java:87-89. A truncated buffer is indistinguishable from a legitimatelynullvalue. If that ambiguity matters, checkgetBytesLength()(:109-111) against your buffer yourself. StringSerializer/StringDeserializerbreak thenullcontract. Both NPE onnullinput (StringSerializer.java:57-59,StringDeserializer.java:69-71) while every other codec returnsnull. SinceStringis in the SPI files (Serializerfile line 9,Deserializerfile line 9), aserializers.getHighestPriority(String.class).serialize(null)call throws where the analogousIntegercall does not.Charsetis not encoded in the bytes.StringSerializerwrites exactlysource.getBytes(charset)with no BOM, length prefix, or charset marker (io/serializer/StringSerializer.java:57-59). TheStringandUTF_16tests pass only because the sameCharsetobject was supplied on both sides (StringSerializerAndDeserializerTest.java:48-57,66-73). Mixing charsets across a wire is silent corruption.EnumSerializeris not discoverable, and encodesordinal()notname(). It is absent from both SPI files, so you must construct it per enum type (io/serializer/EnumSerializer.java:27-31). It writese.ordinal()(:49-59) and reads back viaenums[ordinal](:69-70), which means: reordering or removing enum constants silently reassigns the meaning of stored bytes, and an out-of-range ordinal throwsArrayIndexOutOfBoundsExceptionrather than anIOException.EnumSerializeris also the only codec that does not extendAbstractSerializer(:17), so its width guard is hand-rolled.Serializers/Deserializersare not thread-safe. The registry maps are plainHashMap(io/serializer/Serializers.java:60viaMapUtils.newHashMap(), which returnsnew HashMap<>()atio/collection/MapUtils.java:478-480) andloadSPI()mutates them with a non-atomiccomputeIfAbsent+add(io/serializer/Serializers.java:76-77). Build the registry during startup on one thread; the read methods (get,getHighestPriority,getMostCompatible) are then safe to share because they only read.loadSPI()throws if the SPI file has no providers.ServiceLoaderUtils.loadServicesAsListraisesIllegalArgumentExceptionwhen the iterator is empty (io/util/ServiceLoaderUtils.java:882-886). This is one of the package's few genuine throws, and it fires fromnew Serializers().loadSPI()if you strip the service files (e.g. through a shade plugin that dropsMETA-INF/services). Note it is not thrown for an unknown type — that just givesnull/emptyList().getMostCompatiblereturns a rawSerializer<?>. (io/serializer/Serializers.java:87) — you lose the type parameter and must cast at the call site, exactly asSerializersAndDeserializersTest.java:60-64does.- The unchecked-exception sites are in
convert, not here. Every failure path insideio.serializeris a declaredIOExceptionor a neutral value. The one place a serialization failure becomes unchecked isByteArrayToObjectConverter.doConvert, which catchesIOExceptionand throwsnew RuntimeException(e)(io/convert/ByteArrayToObjectConverter.java:42-44) — and casts the result toSerializable(:41), adding aClassCastExceptionpath. CatchRuntimeExceptionif you go throughConverters.
- I/O, NIO, scanning and file filters —
FastByteArrayInputStream/FastByteArrayOutputStream, the buffers bothDefault*classes wrap, and the resource-close discipline of theiopackage. - Type conversion —
ObjectToByteArrayConverterandByteArrayToObjectConverter, the two converters that expose this package's JDK-serialization path through theConverterSPI. - Reflection, types and beans —
JavaType/TypeUtils.resolveTypeArgumentClasses, which decide the registry key for every provider, andMethodUtils.invokeStaticMethod, whichEnumSerializeruses to callvalues(). - Class loading and artifacts —
ClassLoaderUtils.getDefaultClassLoader()andServiceLoaderUtils, which back the per-ClassLoaderregistry and its caching switch. - Developer Guide — Serialization — the selection algorithm, the registry loop, design patterns, thread-safety analysis and extension points.