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)
}
  1. Every @Mapped class must declare a public no-arg constructor — the generated mapper calls it from fromJson / 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

AnnotationPurpose

@JsonProperty("k")

Rename the JSON key. Default: the field name. aliases lists other keys it’s read from, and required = true refuses JSON that leaves it out.

@JsonIgnore

Omit the field from JSON.

@JsonOmitNull

Leave the field out of JSON while it’s null. On the class, for every field.

@JsonFormat(STRING)

Write a number in quotes, or a Date as an ISO-8601 instant. Either form is read back.

@JsonAdapter(A.class)

Convert the value with your own JsonTypeAdapter. On a class, wherever the class appears.

@JsonStrict

On the class: refuse JSON with a member it doesn’t declare.

@JsonCreator

On a constructor or static factory: build an immutable class from JSON through it, each parameter named with @JsonProperty.

@XmlElement("name")

Rename the XML element. Default: the field name.

@XmlAttribute("name")

Lift the field into an XML attribute rather than a nested child element. Only valid for scalar or Property<scalar> fields.

@XmlTransient

Omit the field from XML.

Supported field types

KindDetail

Scalars

String, all primitives, boxed primitives, java.util.Date, byte[] (base64 in JSON / XML).

Property<T, ?>

T must be a scalar or another @Mapped type.

ListProperty<T, ?>

Same rules as Property.

java.util.List<T>

Read from the field’s generic signature.

Arrays

One dimension, of any type above; a JSON array. XML leaves them out.

Generic classes

A field Page<User> of a @Mapped class Page<T> maps its T members as User.

Nested @Mapped ref.

Recursive serialization through the registered nested mapper.

The server’s generated JSON codecs read the same annotations, so a class shared by the app and its backend has one JSON contract on both sides. JSON that breaks it, such as a missing required member or a value of the wrong type, is refused with an IllegalArgumentException that names where it was: $.items[2].id: required member is missing. A member that’s absent leaves the field as the constructor set it. An explicit null sets the field to null in a @JsonStrict class, on a required member and through an adapter, which is handed the null, as the server does in every class. Elsewhere the app keeps reading an explicit null as it always has, like an absent member, so a class that uses none of these options maps exactly as before.

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:

  1. cn1:process-annotations (PROCESS_CLASSES phase) scans target/classes for every class carrying @Mapped.

  2. 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.

  3. A <SimpleName>Cn1Mapper class is generated for every @Mapped type in the same package as the source class (e.g. com.example.UserCn1Mapper next to com.example.User). Each generated mapper exposes a public static register() hook.

  4. A single cn1app.MapperBootstrap is generated whose constructor calls UserCn1Mapper.register(), ItemCn1Mapper.register(), …​ for every accepted @Mapped class.

At app start:

  • On iOS / Android, the build server probes the project zip for cn1app/MapperBootstrap.class. When found it splices new cn1app.MapperBootstrap(); into the per-build application stub right before Display.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#postInit loads the bootstrap via Class.forName("cn1app.MapperBootstrap"). The classloader is the legitimate path here — JavaSE runs unobfuscated and the same Class.forName pattern 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.