Skip to content

MetaObjectSerializer: DATE branch serializes the containing object, causing unbounded recursion #275

Description

@dmealing

Summary

MetaObjectSerializer.writeField passes the containing object to context.serialize(...) in the DATE branch instead of the field's value. Because the serializer is registered against the object's own class, that call re-enters the same serializer with the same instance, so any MetaObjectAware VO carrying a field.date / field.timestamp self-recurses without bound and dies with a StackOverflowError.

Location

server/java/metadata/src/main/java/com/metaobjects/io/object/gson/MetaObjectSerializer.java

protected void writeField(MetaObject mo, MetaField mf, Object vo,
                          JsonObject jsonObject, JsonSerializationContext context) {
    String name = getJsonName(mf);
    switch (mf.getDataType()) {
        case BOOLEAN:
            jsonObject.addProperty(name, mf.getBoolean(vo));   // reads the FIELD off vo
            break;
        ...
        case DATE:      // TODO: consider custom DATE serialization
            jsonObject.add(name, context.serialize(vo));       // ← serializes vo ITSELF
            break;

Every sibling branch extracts the field value with an mf.getX(vo) accessor. DATE alone hands vo — the whole containing object — straight back to Gson.

Why it recurses

MetaObjectGsonInitializer registers a MetaObjectSerializer for the VO's concrete class (and for its interface, when the object class is one):

builder.registerTypeAdapter(clazz, new MetaObjectSerializer(mo));

So the cycle is:

serialize(vo)writeObject(mo, vo, …)writeField(mo, mf, vo, …)case DATE:context.serialize(vo) → Gson dispatches on vo.getClass() → the same registered MetaObjectSerializerserialize(vo) → …

There is no base case, so it terminates only by exhausting the stack.

Impact

StackOverflowError is an Error, not an Exception. Callers that wrap serialization in catch (Exception …) — the normal shape for a best-effort/telemetry write — do not catch it, so it propagates out of the serialization seam and kills whatever operation was in flight rather than degrading to a skipped write. Any adopter serializing a VO with a date-typed field hits this on the first such object.

Suggested fix

Extract the field value the same way every other branch does. MetaField.getDate(Object) already exists:

case DATE:
    jsonObject.add(name, context.serialize(mf.getDate(vo)));
    break;

Worth deciding at the same time what the intended wire form is — the existing // TODO: consider custom DATE serialization suggests this branch was never finished. Gson's default Date handling is locale-dependent; an ISO-8601 string would round-trip more predictably. Note the deserializer currently groups case DATE: with case LONG:, so whichever form is chosen should be matched there.

Versions

Found against 7.20.11 (Maven Central, Java line). Still present on the current Java line at 7.20.16 — the file's last change was an unrelated hygiene commit.

Notes

Not currently reachable in the adopting codebase where this was found, because no date-typed field is declared in the metadata that goes through this path. Filing it because it is a latent trap: the first field.date anyone adds to a serialized VO turns a working system into a crashing one, with a stack trace that points at Gson rather than at the metadata change that caused it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions