Skip to content

Revert changes to toString() in FieldError #30799

@asjp1970

Description

@asjp1970

Affects: 5.3.27 and later versions


In spring-framework 5.3.27 ObjectUtils.nullSafeConciseToString() method was introduced (#30287), that changed the way to generate null-safe, concise string representation of a supplied object.

We are indirectly using that method from spring-context via FieldError.toString(), to retrieve the string representation of a field failing JSR303 bean validation.

When upgrading from 5.3.24 to 5.3.27 we have had contract tests failing because in case of empty lists that should have at least 1 element, failing validation, the value returned for them was not anymore the expected [], but ArrayList@[the_address].

I'll try to summarize:

Imagine you are in FieldError because the Spring validation wrapping stuff has produced a MethodArgumentNotValidException, the exception handler in the application catches it, and it calls FieldError.toString() to fill in a ProblemDetails.

With 5.3.24:

// snippet in the ApplicationResponseExceptionHandler:

private String getFieldErrorStr(final FieldError fieldErr) {
    String result = "";
    if (fieldErr != null) {
      final String fullErr = fieldErr.toString();
      result = fullErr.substring(0, fullErr.indexOf(";"));
    }
    return result;
  }

// calling this in FieldError:
@Override
public String toString() {
  return "Field error in object '" + getObjectName() + "' on field '" + this.field +
    "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
	resolvableToString();
}

// ...which in the case of an empty collection ended up here in org.springframework.util.ObjectUtils:
public static String nullSafeToString(@Nullable Object obj) {
  if (obj == null) {
    return NULL_STRING;
  }
  if (obj instanceof String) {
    return (String) obj;
  }
  if (obj instanceof Object[]) {
   return nullSafeToString((Object[]) obj);
  }
  if (obj instanceof boolean[]) {
    return nullSafeToString((boolean[]) obj);
  }
  if (obj instanceof byte[]) {
    return nullSafeToString((byte[]) obj);
  }
  if (obj instanceof char[]) {
    return nullSafeToString((char[]) obj);
  }
  if (obj instanceof double[]) {
    return nullSafeToString((double[]) obj);
  }
  if (obj instanceof float[]) {
    return nullSafeToString((float[]) obj);
  }
  if (obj instanceof int[]) {
    return nullSafeToString((int[]) obj);
  }
  if (obj instanceof long[]) {
    return nullSafeToString((long[]) obj);
  }
  if (obj instanceof short[]) {
    return nullSafeToString((short[]) obj);
  }
  String str = obj.toString();
  return (str != null ? str : EMPTY_STRING);
}

For something like a list implementation, when empty, all the if's failed and the call to obj.toString() before the return statement ended in java.util.AbstractCollection.toString (that is the case of java.util.ArrayList), which produced the expected result:

public String toString() {
  Iterator<E> it = iterator();
  if (! it.hasNext())
    return "[]";
  ...
}

With 5.3.27 (and onwards):

// snippet in the ApplicationResponseExceptionHandler same as before

// calling this in FieldError:
@Override
public String toString() {
  return "Field error in object '" + getObjectName() + "' on field '" + this.field +
    "': rejected value [" + ObjectUtils.nullSafeConciseToString(this.rejectedValue) + "]; " +
	resolvableToString();
}

// ...which in the case of an empty collection ended up here in org.springframework.util.ObjectUtils:
public static String nullSafeConciseToString(@Nullable Object obj) {
   if (obj == null) {
    return "null";
  }
  if (obj instanceof Class<?>) {
    return ((Class<?>) obj).getName();
  }
  if (obj instanceof CharSequence) {
    return StringUtils.truncate((CharSequence) obj);
  }
  Class<?> type = obj.getClass();
  if (isSimpleValueType(type)) {
     String str = obj.toString();
     if (str != null) {
     return StringUtils.truncate(str);
   }
  }
  return type.getTypeName() + "@" + getIdentityHexString(obj);
}

As before, with an empty list, obj does not enter any of the instanceof if's, neither the isSimpleValueType. As a result ArrayList@12345 is returned, which is wrong and not what we expect as the value making the validation fail, which is an empty list and not something like ArrayList@12345.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: backportedAn issue that has been backported to maintenance branchestype: regressionA bug that is also a regression

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions