Serialize Object to String in Java: Base64 and JSON

Serialize object to String in Java with ObjectOutputStream and Base64 or with Jackson JSON, read it back, and avoid the deserialization security risk.

Two ways to serialize a Java object to a String, Java serialization with Base64 and JSON with Jackson

To serialize an object to a String in Java, we either write it with ObjectOutputStream and encode the bytes with Base64, or we convert it to JSON or XML text with a library such as Jackson. Both ways give a String that we can store or send and turn back into an equal object later.

We need object-to-String conversion when a value has to fit into a text-only place, such as a database VARCHAR column, a cookie, a message queue header, a cache entry or a JSON payload. Text strings are also easier to log and debug than byte arrays.

The following example converts the same Person record both ways. The helper class SerializationUtils is shown in section 1.

Person person = new Person(1, "Lokesh");
String base64 = SerializationUtils.objectToString(person);  // "rO0ABXNy...", 148 characters
String json = new JsonMapper().writeValueAsString(person);  // {"id":1,"name":"Lokesh"}

Notice the difference in size. The Base64 string carries the full class name and field types, whereas the JSON string carries only the data and is six times shorter.

MechanismRequirementsResult for Person(1, “Lokesh”)
Java serialization with ObjectOutputStream, encoded with Base64The class implements Serializable. No external dependency.148 characters, readable only by Java
JSON (or XML) with Jackson or GsonNo Serializable needed. Needs the Jackson or Gson library.24 characters of JSON, readable by any language
JSON encoded with Base64Same as JSON. Base64 is optional.32 characters, safe for URLs and headers with the URL encoder

We go through both ways with deserialization, add Base64 on top of JSON, and spend a full section on the security risk of turning an untrusted String back into an object.

1. Serialize Object to Base64 Encoded String

In the default serialization mechanism in Java, we can write an object to a byte[] if its class implements the Serializable interface. The classes ObjectOutputStream and ObjectInputStream do the work with their writeObject() and readObject() methods. A byte array is not valid text, so we encode it with java.util.Base64, which turns every 3 bytes into 4 printable characters.

Two ways to serialize a Java object to a String, Java serialization with Base64 and JSON with Jackson
Java serialization plus Base64 gives a long Java-only String, JSON gives a short String any system can read

The following example is a Person record and a utility class with one method for each direction. The reading method takes the expected class and installs an ObjectInputFilter, which we explain in section 3.

record Person(int id, String name) implements Serializable {}
class SerializationUtils {

    static String objectToString(Serializable obj) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bytes)) {
            out.writeObject(obj);
        }
        return Base64.getEncoder().encodeToString(bytes.toByteArray());
    }

    static <T> T stringToObject(String data, Class<T> type)
            throws IOException, ClassNotFoundException {
        byte[] bytes = Base64.getDecoder().decode(data);
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            in.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
                    "maxdepth=5;maxbytes=10000;" + type.getName() + ";java.lang.*;!*"));
            return type.cast(in.readObject());
        }
    }
}

We use the utility method objectToString() to convert the object to a Base64 encoded String, and stringToObject() to convert it back.

Person person = new Person(1, "Lokesh");
String encoded = SerializationUtils.objectToString(person);              // "rO0ABXNy..."
Person restored = SerializationUtils.stringToObject(encoded, Person.class); // Person[id=1, name=Lokesh]
boolean equal = restored.equals(person);                                 // true
rO0ABXNyACtjb20uaG93dG9kb2luamF2YS5jb3JlLnNlcmlhbGl6YXRpb24uUGVyc29uAAAAAAAAAAACAAJJAAJpZEwABG5hbWV0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAAAF0AAZMb2tlc2g=

The encoded String is long because it contains metadata about the serialized object, such as the class name com.howtodoinjava.core.serialization.Person, the serialVersionUID and the type of every field. The prefix rO0AB is the Base64 form of the stream header bytes AC ED 00 05, so a String that starts with it is almost always serialized Java data.

Every class in the object graph must be Serializable, otherwise writeObject() throws NotSerializableException. Fields marked transient are skipped. If we need small strings, or other systems must read them, we serialize the object to JSON or XML instead.

2. Serialize Object to JSON or XML

Another good way to serialize an object to String is to convert it to JSON or XML. A big advantage of this approach is that the class does not need to implement Serializable. That is useful when the class comes from legacy code or a third-party library that we cannot modify. JSON is also readable by humans and by services written in other languages.

The following example uses Jackson 3.2.3, whose mapper classes are in the tools.jackson packages. Jackson 2 code looks the same with com.fasterxml.jackson.databind.ObjectMapper, except that its methods throw the checked JsonProcessingException, while Jackson 3 throws the unchecked JacksonException.

Person person = new Person(1, "Lokesh");
JsonMapper mapper = new JsonMapper();
String json = mapper.writeValueAsString(person);          // {"id":1,"name":"Lokesh"}
Person fromJson = mapper.readValue(json, Person.class);   // Person[id=1, name=Lokesh]

Jackson reads records through their canonical constructor, so Person needs no annotations or setters. For a regular class, Jackson uses a no-argument constructor with setters, or a constructor marked with @JsonCreator.

2.1. Serialize Object to XML

For the XML representation, we use XmlMapper from the jackson-dataformat-xml module instead of JsonMapper, as in our XML and JSON conversion guide. Everything else remains the same.

Person person = new Person(1, "Lokesh");
XmlMapper xmlMapper = new XmlMapper();
String xml = xmlMapper.writeValueAsString(person);        // <Person><id>1</id><name>Lokesh</name></Person>
Person fromXml = xmlMapper.readValue(xml, Person.class);  // Person[id=1, name=Lokesh]

2.2. Encode the JSON String With Base64

Irrespective of JSON or XML, we can still apply Base64 to the String when the target does not accept quotes, braces or angle brackets, such as an HTTP header, a cookie or a URL parameter. The encoded String stays much smaller than the serialized one because it contains no class metadata.

JsonMapper mapper = new JsonMapper();
String json = mapper.writeValueAsString(new Person(1, "Lokesh"));
byte[] utf8 = json.getBytes(StandardCharsets.UTF_8);
String encodedJson = Base64.getEncoder().encodeToString(utf8);            // "eyJpZCI6MSwibmFtZSI6Ikxva2VzaCJ9"
String urlSafe = Base64.getUrlEncoder().withoutPadding().encodeToString(utf8); // "eyJpZCI6MSwibmFtZSI6Ikxva2VzaCJ9"
String decodedJson = new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8);
Person decoded = mapper.readValue(decodedJson, Person.class);             // Person[id=1, name=Lokesh]

We always pass StandardCharsets.UTF_8 to getBytes() and to the String constructor, because the no-argument versions used the platform charset before Java 18. The URL encoder replaces + and / with – and _, which matters for longer data. Base64 is an encoding, not encryption, so anyone can decode the value, as explained in our Base64 encoding and decoding guide.

3. Deserializing an Object From an Untrusted String

Never pass a String from a user, a cookie, a request parameter or any other system we do not control to ObjectInputStream.readObject() without a filter. Java deserialization creates objects of any Serializable class on the classpath that the data names, and it runs code in those classes, such as their readObject() methods, before our code ever sees the result. Attackers combine such classes from common libraries into gadget chains, where one class calls a method on the next, until the last call runs a system command.

A typical case shows how this happens. A web app keeps the shopping cart in a cookie as a Base64 serialized object. An attacker replaces the cookie value with a crafted stream, and the server deserializes it on the next request before any login check runs. Several remote code execution vulnerabilities in widely used Java products came from this pattern of reading untrusted data with readObject().

The JDK offers ObjectInputFilter (Java 9, JEP 290) to limit what a stream may contain. The filter in stringToObject() allows only the expected class and java.lang classes, limits the depth of the object graph and the stream size, and rejects everything else with !*.

Map<String, Integer> other = new HashMap<>(Map.of("a", 1));
String foreign = SerializationUtils.objectToString((Serializable) other);
Person rejected = SerializationUtils.stringToObject(foreign, Person.class);  // InvalidClassException: filter status: REJECTED

The filter stops the HashMap before any of its code runs. For a whole application, the system property jdk.serialFilter sets a JVM-wide filter with the same pattern syntax, and Java 17 added a filter factory for per-stream rules (JEP 415). The OWASP deserialization cheat sheet gives the same advice and recommends data-only formats for anything that crosses a trust boundary.

JSON with a concrete target type, such as readValue(json, Person.class), is the safer choice, because Jackson only creates the classes that Person declares. That stays true as long as we do not enable polymorphic default typing, which lets the JSON name its own classes and brings back the same class of risk.

4. Choosing Between Serialization and JSON

For most apps, JSON is the better default. It is shorter, readable in logs and by other languages, and it does not break when we rename a private field, as long as the JSON property names stay the same. Java serialization still has a place when both sides are Java code that we control, for example when we deep copy an object graph or keep a session in a trusted, internal cache.

NeedJava serialization + Base64JSON with Jackson
Other languages read the dataNoYes
String sizeLarge, includes class metadataSmall, data only
Class changesNeeds a matching serialVersionUIDTolerant if property names stay the same
Untrusted inputDangerous without ObjectInputFilterSafe with a concrete target type
DependenciesNone, part of the JDKJackson or Gson library
Object graphs with cyclesSupportedNeeds extra annotations

Java serialization handles shared references and cycles in an object graph, whereas JSON writes a tree and needs annotations such as @JsonIdentityInfo for the same result. For collections, the ArrayList serialization example shows the Java serialization side in more detail.

5. Object to String Serialization FAQs

Developers who move objects into text columns, caches and cookies ask the same practical questions.

5.1. Is calling toString() the same as serializing an object?

No. The toString() method returns a description for humans, such as Person[id=1, name=Lokesh], and Java has no standard way to parse it back into an object. Serialization produces a format that a matching reader turns back into an equal object.

5.2. Why is the Base64 serialized String so long?

Java serialization writes a stream header, the full class name, the serialVersionUID and the name and type of every field before the data itself. Base64 adds one third on top, so our two-field Person becomes 110 bytes and 148 characters, whereas the JSON form has 24 characters.

5.3. How do we store a serialized object in a database?

We store JSON in a text or JSON column, such as jsonb in PostgreSQL, so the database can index and query it. If we must keep Java serialized data, we store the raw bytes in a BLOB or bytea column instead of a Base64 String, which saves a third of the space.

5.4. Can we serialize an object that does not implement Serializable?

Not with ObjectOutputStream, which throws NotSerializableException. JSON and XML libraries such as Jackson and Gson do not need the interface, so we use them to convert third-party classes.

5.5. Is Base64 encoding a form of encryption?

No. Base64 only maps bytes to printable characters, and anyone can decode it. To protect the content, we encrypt the bytes before encoding them, for example with AES-GCM, or we sign them with an HMAC when we only need to detect tampering.

6. Conclusion

We can turn a Java object into a String in two ways. Java serialization plus Base64 needs no library and keeps the full object graph, but the String is long, readable only by Java and dangerous to read back from untrusted sources without an ObjectInputFilter. JSON or XML with Jackson gives a short, portable String, works with classes that are not Serializable, and can be Base64 encoded when the target accepts only plain characters.

For data that leaves our own JVMs, such as cookies, queues and APIs, JSON with a concrete target type is the safe default. We keep Java serialization for trusted, Java-only uses, and we always install a filter when reading it.

7. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.