Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StackOverflowError for enum serialization since 2.18.2 #4906

Closed
1 task done
nvervelle opened this issue Jan 15, 2025 · 8 comments
Closed
1 task done

StackOverflowError for enum serialization since 2.18.2 #4906

nvervelle opened this issue Jan 15, 2025 · 8 comments
Labels
to-evaluate Issue that has been received but not yet evaluated

Comments

@nvervelle
Copy link

Search before asking

  • I searched in the issues and found nothing similar.

Describe the bug

When trying to upgrade my project with spring-boot 3.4.1 (which brings Jackson 2.18.2) instead of spring-boot 3.4.0, my project build fails in many places due to StackOverflowError when serializing values, for enum.

By looking at the stacktrace, it seems that it's because :

  • We use Jackson serialization in the toString() method of the enum : so that the toString() method would return the same value as the one that will be used when serializing to JSON...
  • And with 2.18.2, the serialization of the enum now calls the toString() method of the enum => which leads to recursive calls and the stack overflow...

Is it voluntary that this constructs now fails with 2.18.2 even if it was working with 2.18.1 ?
If it's voluntary or will stay that way, how can we achieve the same behavior as before ? (the toString() method returning the same value as the serialized value specified with @JsonProperty

Version Information

2.18.2

Reproduction

public enum Example {
  @JsonProperty("example1")
  EXAMPLE_1,
  @JsonProperty("example2")
  EXAMPLE_2;

  @Override
  public String toString() {
    return OBJECT_MAPPER.convertValue(this, String.class);
  }
}

Expected behavior

No response

Additional context

No response

@nvervelle nvervelle added the to-evaluate Issue that has been received but not yet evaluated label Jan 15, 2025
@nvervelle nvervelle changed the title StackOverflowError since 2.18.2 StackOverflowError for enum serialization since 2.18.2 Jan 15, 2025
@pjfanning
Copy link
Member

Please provide the stackoverflowerror and trace. We don't need full trace but include a couple of cycles where the code is presumably running through the same set of calls.

@nvervelle
Copy link
Author

Oups... sorry @pjfanning , I had prepared it and put it in the description, but it seems I deleted it somehow...

    java.lang.StackOverflowError
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.drainReadBuffer(PrivateMaxEntriesMap.java)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.drainReadBuffers(PrivateMaxEntriesMap.java:402)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.drainBuffers(PrivateMaxEntriesMap.java:392)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.tryToDrainBuffers(PrivateMaxEntriesMap.java:381)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.drainOnReadIfNeeded(PrivateMaxEntriesMap.java:358)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.afterRead(PrivateMaxEntriesMap.java:315)
        at com.fasterxml.jackson.databind.util.internal.PrivateMaxEntriesMap.get(PrivateMaxEntriesMap.java:622)
        at com.fasterxml.jackson.databind.util.LRUMap.get(LRUMap.java:69)
        at com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector.isAnnotationBundle(JacksonAnnotationIntrospector.java:165)
        at com.fasterxml.jackson.databind.introspect.CollectorBase.collectAnnotations(CollectorBase.java:40)
        at com.fasterxml.jackson.databind.introspect.AnnotatedFieldCollector._findFields(AnnotatedFieldCollector.java:86)
        at com.fasterxml.jackson.databind.introspect.AnnotatedFieldCollector.collect(AnnotatedFieldCollector.java:48)
        at com.fasterxml.jackson.databind.introspect.AnnotatedFieldCollector.collectFields(AnnotatedFieldCollector.java:43)
        at com.fasterxml.jackson.databind.introspect.AnnotatedClass._fields(AnnotatedClass.java:370)
        at com.fasterxml.jackson.databind.introspect.AnnotatedClass.fields(AnnotatedClass.java:342)
        at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector._addFields(POJOPropertiesCollector.java:527)
        at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.collectAll(POJOPropertiesCollector.java:442)
        at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.getJsonValueAccessor(POJOPropertiesCollector.java:269)
        at com.fasterxml.jackson.databind.introspect.BasicBeanDescription.findJsonValueAccessor(BasicBeanDescription.java:248)
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.findSerializerByAnnotations(BasicSerializerFactory.java:393)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory._createSerializer2(BeanSerializerFactory.java:225)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory.createSerializer(BeanSerializerFactory.java:174)
        at com.fasterxml.jackson.databind.SerializerProvider._createUntypedSerializer(SerializerProvider.java:1523)
        at com.fasterxml.jackson.databind.SerializerProvider._createAndCacheUntypedSerializer(SerializerProvider.java:1471)
        at com.fasterxml.jackson.databind.SerializerProvider.findValueSerializer(SerializerProvider.java:578)
        at com.fasterxml.jackson.databind.SerializerProvider.findTypedValueSerializer(SerializerProvider.java:856)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:330)
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4614)
        at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4567)
        at xxx.FieldOrientation.toString(FieldOrientation.java:18)
        at com.fasterxml.jackson.databind.util.EnumValues.constructFromToString(EnumValues.java:117)
        at com.fasterxml.jackson.databind.ser.std.EnumSerializer.construct(EnumSerializer.java:125)
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.buildEnumSerializer(BasicSerializerFactory.java:1218)
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.findSerializerByPrimaryType(BasicSerializerFactory.java:428)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory._createSerializer2(BeanSerializerFactory.java:235)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory.createSerializer(BeanSerializerFactory.java:174)
        at com.fasterxml.jackson.databind.SerializerProvider._createUntypedSerializer(SerializerProvider.java:1523)
        at com.fasterxml.jackson.databind.SerializerProvider._createAndCacheUntypedSerializer(SerializerProvider.java:1471)
        at com.fasterxml.jackson.databind.SerializerProvider.findValueSerializer(SerializerProvider.java:578)
        at com.fasterxml.jackson.databind.SerializerProvider.findTypedValueSerializer(SerializerProvider.java:856)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:330)
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4614)
        at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4567)
        at xxx.FieldOrientation.toString(FieldOrientation.java:18)
        at com.fasterxml.jackson.databind.util.EnumValues.constructFromToString(EnumValues.java:117)
        at com.fasterxml.jackson.databind.ser.std.EnumSerializer.construct(EnumSerializer.java:125)
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.buildEnumSerializer(BasicSerializerFactory.java:1218)
        at com.fasterxml.jackson.databind.ser.BasicSerializerFactory.findSerializerByPrimaryType(BasicSerializerFactory.java:428)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory._createSerializer2(BeanSerializerFactory.java:235)
        at com.fasterxml.jackson.databind.ser.BeanSerializerFactory.createSerializer(BeanSerializerFactory.java:174)
        at com.fasterxml.jackson.databind.SerializerProvider._createUntypedSerializer(SerializerProvider.java:1523)
        at com.fasterxml.jackson.databind.SerializerProvider._createAndCacheUntypedSerializer(SerializerProvider.java:1471)
        at com.fasterxml.jackson.databind.SerializerProvider.findValueSerializer(SerializerProvider.java:578)
        at com.fasterxml.jackson.databind.SerializerProvider.findTypedValueSerializer(SerializerProvider.java:856)
        at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:330)
        at com.fasterxml.jackson.databind.ObjectMapper._convert(ObjectMapper.java:4614)
        at com.fasterxml.jackson.databind.ObjectMapper.convertValue(ObjectMapper.java:4567)
        at xxx.FieldOrientation.toString(FieldOrientation.java:18)

@pjfanning
Copy link
Member

Looks like this change may be the cause of the change.

https://github.com/FasterXML/jackson-databind/blame/2.18/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java#L117

#4788 @JooHyukKim @cowtowncoder It does see to me calling objectMapper.convertValue in a toString is not a great idea even if it worked before.

@nvervelle
Copy link
Author

Ok, thanks. If it's not a great idea to call convertValue() in toString(), what would be the best way to have the toString() method return the value set in @JsonProperty ?

@pjfanning
Copy link
Member

I guess I was asking the opinion of @JooHyukKim and @cowtowncoder.

One enhancement that we could add to https://github.com/FasterXML/jackson-databind/blame/2.18/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java#L117 would be to check if the enum instance has a JsonProperty annotaton before checking its toString.

@nvervelle you could also check the for the JsonProperty annotation in your toString call. I would expect some trial and error trying to find the best way to track down the annotation.

@cowtowncoder
Copy link
Member

cowtowncoder commented Jan 15, 2025

Uhhh. This:

  public String toString() {
    return OBJECT_MAPPER.convertValue(this, String.class);
  }

is a HORRIBLE, TERRIBLE, NO GOOD idea. Please, please do not do that.
(it's bound to hit issues, functionality-wise, but is also bad for performance)

Calling ObjectMapper methods in general from toString() is a bad idea.

Any code that does that is basically something that voids any warranties.

So: while there is no intended change to break things, I think I will close this as "wont fix". Calling convertValue() (or writeValue() and variants) from toString() method of Enum is not supported thing to do.

As to how to avoid duplication: I am not sure. If toString() must be jiggered, inspecting annotation directly from it, without Jackson's help, is better (even if more code). I realize it is more code to write, but basically current way is not supported and no code changes would be made to support it reliably
(that is, even if we tweaked code to work around the problem, this is very fragile set up)

@nvervelle
Copy link
Author

nvervelle commented Jan 16, 2025

Ok, I'm replacing the code for the toString() method to retrieve the @JsonProperty annotation directly.

If anyone is interested, here's my utility class for that

public final class EnumUtil {

  /** Constructor. Private to avoid instantiation */
  private EnumUtil() {}

  public static <E extends Enum<E>> String getJsonValue(final E value) {
    try {
      final JsonProperty jsonProperty =
          value.getDeclaringClass().getField(value.name()).getAnnotation(JsonProperty.class);
      if (jsonProperty == null) {
        return value.name();
      }
      return jsonProperty.value();
    } catch (NoSuchFieldException e) {
      return value.name();
    }
  }
}

@cowtowncoder
Copy link
Member

@nvervelle Great! Thank you for sharing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
to-evaluate Issue that has been received but not yet evaluated
Projects
None yet
Development

No branches or pull requests

3 participants