Codename One includes a build-time POJO mapping framework that turns a
plain Java class — or a PropertyBusinessObject — into JSON or XML
without any runtime reflection. The Maven plugin scans the project’s
compiled bytecode at build time, generates a typed Mapper next to each
@Mapped class, and ships it alongside the source. The runtime entry
point is two methods on com.codename1.mapping.Mappers.
The framework sits next to the existing JSONParser, XMLParser and
PropertyIndex.toJSON / fromJSON helpers — it doesn’t replace them.
Use the imperative APIs when you want to read a foreign JSON shape
without modelling it; use the annotation framework when you have a model
class and want a one-liner round-trip.
Annotate the model
Apply @Mapped to the class. The processor accepts POJOs with public
fields, PropertyBusinessObject-style classes with Property fields,
or any mix:
@Mapped
@XmlRoot("user")
public class User {
@JsonProperty("first_name")
@XmlElement("first")
public String firstName;
public int age;
// Renders as <user role="admin"/> in XML; omitted from JSON.
@XmlAttribute
@JsonIgnore
public String role;
public User() { } // (1)
}Every
@Mappedclass must declare a public no-arg constructor — the generated mapper calls it fromfromJson/fromXml.
For PropertyBusinessObject-style classes the same annotations work on
Property<T> fields. The processor reads through Property#get /
Property#set so subscribers (addChangeListener) still see the
mutation:
@Mapped
public class Item implements PropertyBusinessObject {
public final Property<String, Item> name = new Property<>("name");
public final Property<Integer, Item> qty = new Property<>("qty");
private final PropertyIndex idx =
new PropertyIndex(this, "Item", name, qty);
public PropertyIndex getPropertyIndex() { return idx; }
public Item() { }
}
Field-level annotations
| Annotation | Purpose |
|---|---|
| Rename the JSON key. Default: the field name. |
| Omit the field from JSON. |
| Rename the XML element. Default: the field name. |
| Lift the field into an XML attribute rather
than a nested child element. Only valid for
scalar or |
| Omit the field from XML. |
Supported field types
| Kind | Detail |
|---|---|
Scalars |
|
|
|
| Same rules as |
| Read from the field’s generic signature. |
Nested | Recursive serialization through the registered nested mapper. |
Round-trip
Mappers.fromJson and Mappers.fromXml both accept either a String
or a java.io.Reader, so streamed responses (network, file, resource)
don’t need to be fully buffered first.
How the plumbing works
At build time:
cn1:process-annotations(PROCESS_CLASSES phase) scanstarget/classesfor every class carrying@Mapped.The processor validates each one (concrete, public no-arg constructor, supported field types) and fails the build on the first offender — every offending class shows up in the same error report.
A
<SimpleName>Cn1Mapperclass is generated for every@Mappedtype in the same package as the source class (e.g.com.example.UserCn1Mappernext tocom.example.User). Each generated mapper exposes a publicstatic register()hook.A single
cn1app.MapperBootstrapis generated whose constructor callsUserCn1Mapper.register(),ItemCn1Mapper.register(), … for every accepted@Mappedclass.
At app start:
On iOS / Android, the build server probes the project zip for
cn1app/MapperBootstrap.class. When found it splicesnew cn1app.MapperBootstrap();into the per-build application stub right beforeDisplay.init. ParparVM rename and R8 obfuscation rewrite the call site and the generated class together, so the direct symbol reference stays valid after the pass.On the JavaSE simulator and desktop run,
JavaSEPort#postInitloads the bootstrap viaClass.forName("cn1app.MapperBootstrap"). The classloader is the legitimate path here — JavaSE runs unobfuscated and the sameClass.forNamepattern is used by the @Route dispatcher.
Projects that ship no @Mapped classes produce no bootstrap, the build
server probe falls through, and JavaSEPort catches the
ClassNotFoundException — the registry stays empty and Mappers.toJson
throws a clear "no mapper registered" error.
The runtime registry is keyed on Class#getName(). The map keys survive
obfuscation because both the registration site and the lookup site see
the same renamed name within a single execution — the keys are never
persisted across builds.
Custom mappers
Sometimes a class lives in a third-party JAR the build can’t annotate.
Hand-write a Mapper<T> and register it at startup:
// A type from a third-party jar the build cannot annotate.
static class LatLon {
double lat;
double lon;
}
static class LatLonMapper implements Mapper<LatLon> {
@Override
public Class<LatLon> type() {
return LatLon.class;
}
@Override
public Map<String, Object> toMap(LatLon instance) {
Map<String, Object> m = new LinkedHashMap<String, Object>();
m.put("lat", Double.valueOf(instance.lat));
m.put("lon", Double.valueOf(instance.lon));
return m;
}
@Override
public LatLon fromMap(Map<String, Object> map) {
LatLon out = new LatLon();
out.lat = readDouble(map.get("lat"));
out.lon = readDouble(map.get("lon"));
return out;
}
@Override
public String xmlRootName() {
return "latLon";
}
@Override
public void writeXml(LatLon instance, Element root) {
root.setAttribute("lat", String.valueOf(instance.lat));
root.setAttribute("lon", String.valueOf(instance.lon));
}
@Override
public LatLon readXml(Element root) {
LatLon out = new LatLon();
out.lat = Double.parseDouble(root.getAttribute("lat"));
out.lon = Double.parseDouble(root.getAttribute("lon"));
return out;
}
// JSONParser hands back a Double for every number, but a map that came
// from somewhere else may hold any Number. Read through the interface
// rather than casting to Double: a failed cast does not throw on iOS,
// so the catch you would write for it never runs.
private double readDouble(Object value) {
return value instanceof Number ? ((Number) value).doubleValue() : 0;
}
}
void registerMappers() {
Mappers.register(new LatLonMapper());
}
Hand-written mappers take precedence over generated ones for the same class.