There are several good options for java object serialization and deserialization. One of these is to use the YAML data serialization standard.
YAML™ (rhymes with “camel“) is a human-friendly, cross language, Unicode based data serialization language designed around the common native data types of agile programming languages. It is broadly useful for programming needs ranging from configuration files to Internet messaging to object persistence to data auditing.
There are also several Java libraries that implement the YAML standard (as listed on yaml.org).
- JvYaml
- SnakeYAML
- YamlBeans
- JYaml
For the purpose of this article we’ll be using SnakeYAML as a solid modern implementation of the YAML 1.1 standard.
Consider the following simple example:
25 26 27 28 29 30 31 |
public class Weapon { private int dps; private String model; // getters/setters omitted for brevity } |
Source: Weapon.java
This Java bean can be serialized and deserialized with just the following code:
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
public class WeaponTest { @Test public void testSerialization() { Weapon primary = new Weapon(10, "m4a1"); Yaml yamlProcessor = new Yaml(); String yaml = yamlProcessor.dump(primary); System.out.println(yaml); Weapon primaryCopy = yamlProcessor.loadAs(yaml, Weapon.class); Assert.assertFalse(primary == primaryCopy); Assert.assertEquals(primary, primaryCopy); } } |
Source: WeaponTest.java
Simple Java beans are automatically serialized and deserialized, the data looks like this:
1 |
!!io.techgarage.Weapon {dps: 10, model: m4a1} |
Of course in reality things are not always that simple. Your domain model may contain types that are not Java beans like java.net.URL, java.util.UUID, java.net.InetAddress and others. In this case the serialization process will ignore the non-bean objects and deserialization will fail altogether.
Fortunately SnakeYAML’s represent and construct functionality can easily be extended. Consider the following more complex example:
29 30 31 32 33 34 35 36 37 |
public class Cyborg { private UUID id; private String model; private InetAddress address; private Collection<Weapon> weapons; // getters/setters omitted for brevity } |
Source: Cyborg.java
Now we have non-bean types and java.util.Collection. To handle serialization and deserialization properly we need to use custom representer and constructor.
The representer takes the non-bean objects and transforms them into custom-tagged scalars. Also RepresentInetAddress is registered for both Inet4Address and Inet6Address to handle both protocols.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public class ApplicationRepresenter extends Representer { private class RepresentUUID implements Represent { @Override public Node representData(Object o) { UUID uuid = (UUID) o; return representScalar(new Tag("!uuid"), uuid.getMostSignificantBits() + " " + uuid.getLeastSignificantBits()); } } private class RepresentInetAddress implements Represent { @Override public Node representData(Object o) { InetAddress inetAddr = (InetAddress) o; return representScalar(new Tag("!inet"), inetAddr.getHostAddress()); } } public ApplicationRepresenter() { this.representers.put(UUID.class, new RepresentUUID()); this.representers.put(Inet4Address.class, new RepresentInetAddress()); this.representers.put(Inet6Address.class, new RepresentInetAddress()); } } |
Source: ApplicationRepresenter.java
The constructor does exactly the opposite of the representer does.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
public class ApplicationConstructor extends Constructor { private class ConstructUUID extends AbstractConstruct { @Override public Object construct(Node node) { Object o = constructScalar((ScalarNode) node); String[] uuidBits = o.toString().split(" "); return new UUID(Long.parseLong(uuidBits[0]), Long.parseLong(uuidBits[1])); } } private class ConstructInetAddress extends AbstractConstruct { @Override public Object construct(Node node) { Object o = constructScalar((ScalarNode) node); try { return InetAddress.getByName(o.toString()); } catch (UnknownHostException ex) { throw new IllegalArgumentException("Bad IP address format!", ex); } } } public ApplicationConstructor() { this.yamlConstructors.put(new Tag("!uuid"), new ConstructUUID()); this.yamlConstructors.put(new Tag("!inet"), new ConstructInetAddress()); } } |
Source: ApplicationConstructor.java
Our custom representer and constructor are passed to the constructor of the YAML processor. Putting everything together looks like this:
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public class CyborgTest { @Test public void testSerialization() throws Exception { InetAddress ip = InetAddress.getByName("::1"); // IPv6 loopback address Weapon primary = new Weapon(10, "m4a1"); Weapon sidearm = new Weapon(6, "glock"); Collection<Weapon> weapons = new ArrayList<>(); weapons.add(primary); weapons.add(sidearm); Cyborg cyborg = new Cyborg(UUID.randomUUID(), "TG-1000", ip, weapons); Yaml yamlProcessor = new Yaml(new ApplicationConstructor(), new ApplicationRepresenter()); String yaml = yamlProcessor.dump(cyborg); System.out.println(yaml); Cyborg cyborgCopy = yamlProcessor.loadAs(yaml, Cyborg.class); Assert.assertFalse(cyborg == cyborgCopy); Assert.assertEquals(cyborg, cyborgCopy); } } |
Source: CyborgTest.java
Now the type hierarchy is represented in YAML like this:
1 2 3 4 5 6 7 |
!!io.techgarage.Cyborg address: !inet '0:0:0:0:0:0:0:1' id: !uuid '7761487157133527242 -8447463166146773756' model: TG-1000 weapons: - {dps: 10, model: m4a1} - {dps: 6, model: glock} |
As you can see the non-bean type values are tagged with custom tags: !inet
and !uuid
. That’s how the YAML document is later properly deserialized.