Swap
Swaps are a critical component of Juneau. They allow the serializers and parsers to handle Java objects that wouldn't normally be serializable.
See the guide: Serialize a type Juneau doesn't know
Swaps are, simply put, 'object swappers' that swap in serializable objects for non-serializable ones during serialization, and vis-versa during parsing.
Some examples of non-serializable objects are File, Reader, Iterable, etc...
These are classes that aren't beans and cannot be represented as simple Maps, Collections, or primitives.
In the following example, we introduce a ObjectSwap that will swap in a bean of a particular type with a map
containing customized key-value pairs:
// Sample swap for converting a bean to a specialized map of key-value pairs.
public class MyBeanSwap extends ObjectSwap<MyBean,JsonMap> {
// Converts a bean to a generic map.
@Override /* ObjectSwap */
public JsonMap swap(MarshallingSession session, MyBean bean) {
return JsonMap.of("foo", bean.getBar());
}
// Converts the generic map back into a bean.
@Override /* ObjectSwap */
public MyBean unswap(MarshallingSession session, JsonMap map, ClassMeta<?> hint) throws Exception {
MyBean bean = new MyBean();
bean.setBar(map.getString("foo"));
return bean;
}
}
The swap can then be associated with serializers and parsers like so:
// Create a new JSON serializer with our swap.
WriterSerializer serializer = Json5Serializer.create().swaps(MyBeanSwap.class).build();
String json = serializer.write(new MyBean());
// Create a JSON parser with our swap.
ReaderParser parser = JsonParser.create().swaps(MyBeanSwap.class).build();
MyBean bean = parser.read(json, MyBean.class);
Another example of a ObjectSwap is one that converts byte[] arrays to BASE64-encoded strings:
public class ByteArrayBase64Swap extends StringSwap {
@Override /* StringSwap */
public String swap(MarshallingSession session, byte[] bytes) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
OutputStream b64os = MimeUtility.encode(baos, "base64");
b64os.write(bytes);
b64os.close();
return new String(baos.toByteArray());
}
@Override /* StringSwap */
public byte[] unswap(MarshallingSession session, String string, ClassMeta<?> hint) throws Exception {
byte[] bytes = string.getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
InputStream b64is = MimeUtility.decode(bais, "base64");
byte[] tmp = new byte[bytes.length];
int n = b64is.read(tmp);
byte[] res = new byte[n];
System.arraycopy(tmp, 0, res, 0, n);
return res;
}
}
The following example shows the BASE64 swap in use:
// Create a JSON serializer and register the BASE64 encoding swap with it.
WriterSerializer serializer = Json5Serializer.create().swaps(ByteArrayBase64Swap.class).build();
ReaderParser parser = JsonParser.create().swaps(ByteArrayBase64Swap.class).build();
byte[] bytes = {1,2,3};
String json = serializer.write(bytes); // Produces "'AQID'"
bytes = parser.read(json, byte[].class); // Reproduces {1,2,3}
byte[][] bytes2d = {{1,2,3},{4,5,6},null};
json = serializer.write(bytes2d); // Produces "['AQID','BAUG',null]"
bytes2d = parser.read(json, byte[][].class); // Reproduces {{1,2,3},{4,5,6},null}
The MarshallingContextable.Builder.swap(Class,Class,ThrowingFunction) and MarshallingContextable.Builder.swap(Class,Class,ThrowingFunction,ThrowingFunction) methods are another way to define swaps by using functions.
// Use a function to convert beans to strings.
WriterSerializer serializer = Json5Serializer
.create()
.swap(MyBean.class, String.class, x -> myBeanStringifier(x))
.build();
Per-media-type swaps
Swaps can also be defined per-media-type.
The ObjectSwap.forMediaTypes() method can be overridden to provide a set of media types that the swap is invoked on.
It's also possible to define multiple swaps against the same object as long as they're differentiated by media type.
When multiple swaps are defined, the best-match media type is used.
In the following example, we define 3 swaps against the same object. One for JSON, one for XML, and one for all other types.
public class ObjectSwapTest {
public static class MyPojo {}
public static class MyJsonSwap extends StringSwap {
@Override /* ObjectSwap */
public MediaType[] forMediaTypes() {
return MediaType.forStrings("*/json");
}
@Override /* ObjectSwap */
public String swap(MarshallingSession session, MyPojo pojo) throws Exception {
return "It's JSON!";
}
}
public static class MyXmlSwap extends StringSwap {
@Override /* ObjectSwap */
public MediaType[] forMediaTypes() {
return MediaType.forStrings("*/xml");
}
@Override /* ObjectSwap */
public String swap(MarshallingSession session, MyPojo pojo) throws Exception {
return "It's XML!";
}
}
public static class MyOtherSwap extends StringSwap {
@Override /* ObjectSwap */
public MediaType[] forMediaTypes() {
return MediaType.forStrings("*/*");
}
@Override /* ObjectSwap */
public String swap(MarshallingSession session, MyPojo pojo) throws Exception {
return "It's something else!";
}
}
@Test
public void doTest() throws Exception {
SerializerSet serializers = SerializerSet.create()
.add(JsonSerializer.class, XmlSerializer.class, HtmlSerializer.class)
.forEach(x -> x.swaps(MyJsonSwap.class, MyXmlSwap.class, MyOtherSwap.class))
.forEachWS(x -> x.ws())
.build();
MyPojo myPojo = new MyPojo();
String json = serializers.getWriterSerializer("text/json").orElseThrow().write(myPojo);
assertEquals("'It\\'s JSON!'", json);
String xml = serializers.getWriterSerializer("text/xml").orElseThrow().write(myPojo);
assertEquals("It's XML!", xml);
String html = serializers.getWriterSerializer("text/html").orElseThrow().write(myPojo);
assertEquals("It's something else!", html);
}
}
When multiple swaps match the same media type, a best-match algorithm is applied to find the correct swap to use.
In later sections we describe how annotations can be used to shorten this syntax:
@Swap(MyJsonSwap.class)
@Swap(MyXmlSwap.class)
@Swap(MyOtherSwap.class)
public static class MyPojo {}
@Swap(mediaTypes="*/json")
public static class MyJsonSwap extends ObjectSwap {...}
@Swap(mediaTypes="*/xml")
public static class MyXmlSwap extends ObjectSwap {...}
@Swap(mediaTypes="*/*")
public static class MyOtherSwap extends ObjectSwap {...}
In This Section
📄️ 2.2.13.1. Default Swaps
By default, all serializers and parsers have built in ObjectSwaps defined for the following common data types:
📄️ 2.2.13.2. Auto-detected swaps
Various methods can be defined on a class directly to affect how it gets serialized.
📄️ 2.2.13.3. One-way Swaps
In the previous sections, we defined two-way swaps, meaning swaps where the original objects could be reconstructing
📄️ 2.2.13.4. @Swap Annotation
@Swap can be used to associate a swap class using an annotation.
📄️ 2.2.13.5. Templated Swaps
The @Swap(template) annotation allows you to associate
📄️ 2.2.13.6. Surrogate Classes
Surrogate classes are very similar in concept to ObjectSwaps except they're simpler to define.