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 MetaObjectSerializer → serialize(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.
Summary
MetaObjectSerializer.writeFieldpasses the containing object tocontext.serialize(...)in theDATEbranch 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 anyMetaObjectAwareVO carrying afield.date/field.timestampself-recurses without bound and dies with aStackOverflowError.Location
server/java/metadata/src/main/java/com/metaobjects/io/object/gson/MetaObjectSerializer.javaEvery sibling branch extracts the field value with an
mf.getX(vo)accessor.DATEalone handsvo— the whole containing object — straight back to Gson.Why it recurses
MetaObjectGsonInitializerregisters aMetaObjectSerializerfor the VO's concrete class (and for its interface, when the object class is one):So the cycle is:
serialize(vo)→writeObject(mo, vo, …)→writeField(mo, mf, vo, …)→case DATE:→context.serialize(vo)→ Gson dispatches onvo.getClass()→ the same registeredMetaObjectSerializer→serialize(vo)→ …There is no base case, so it terminates only by exhausting the stack.
Impact
StackOverflowErroris anError, not anException. Callers that wrap serialization incatch (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:Worth deciding at the same time what the intended wire form is — the existing
// TODO: consider custom DATE serializationsuggests this branch was never finished. Gson's defaultDatehandling is locale-dependent; an ISO-8601 string would round-trip more predictably. Note the deserializer currently groupscase DATE:withcase 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.dateanyone 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.