jackson-databind icon indicating copy to clipboard operation
jackson-databind copied to clipboard

Support factories (external factory methods)

Open cns-solutions-admin opened this issue 2 years ago • 2 comments

Feature Support static factory methods in factory classes. The factory method should be able to accept properties like a class-local creator method.

Problem solved Currently a creator method must be within the same class. Factory methods in other classes are not supported. Thus, if the constructor of a non-modifiable class (e.g. from a library) is non-accessible and the factory method is in another class, the only workaround is to use CAN_OVERRIDE_ACCESS_MODIFIERS, which is not possible in some environments.

Examples classes that cannot be deserialized currently in environments, where access modifiers cannot be overridden:

  • Creation via factory:
public class ClassWithoutAccessibleConstructor {
  ...
}

public FactoryForClassWithoutAccessibleConstructor {
  public static createClassWithoutAccessibleConstructor() {
    ...
  }
}
  • Class with Builder without public constructor:
public class ClassWithBuilder {
  public static builder() {
    return new BuilderWithoutPublicConstructor();
  }

  public static class BuilderWithoutPublicConstructor {
  }
}

Proposed implementation

  • extend the @JsonDeserialize annotation by a factory property
  • use @JsonCreator in the factory to mark the factory method
  • use @JsonProperty to mark factory method parameters
  • Mixins are supported for factories, too, to allow configuration, if the factory class cannot be changed

Usage example:

  • class with unaccessible constructor and factory
@JsonDeserialize(factory = MyFactory.class)
public class MyBean {
  ...
}

public class MyFactory {
  @JsonCreator
  public static MyBean createMyBean(@JsonProperty(...) ...) {
    ...
  }
}
  • class with builder that has an unaccessible constructor
@JsonDeserialize(builder = MyBuilder.class)
public class MyBean {
  ...
  @JsonCreator
  public static MyBuilder builder() {
    return new MyBuilder();
  }

  @JsonPOJOBuilder
  @JsonDeserialize(factory = MyBean.class)
  public static class MyBuilder {
    ...
  }
}

Additional context

  • in the second example the bean is at the same time a factory for the builder. If this makes implementation too difficult and will not be allowed, it is easy to implement a simple factory that just calls the bean's builder factory method instead.
  • in the same way it is not necessary to support non-static factory methods, as it is easy to create a new custom factory class with a method that constructs the factory and delegates to the non-static method.
  • this feature request can also solve issue #1820, by using the mixin as factory, too, e.g.:
@JsonDeserialize(factory = MyBeanMixin.class)
public class MyBeanMixin {
  @JsonCreator
  public static createBean(@JsonProperty(...) ...) {
    ...
  }
}
  • this feature request also solves issue #2354 as shown in second example.
  • this feature request also supports a work around for #3041, namely creating a factory method with all parameters of the available constructors, which then decides, which constructor to call.

cns-solutions-admin avatar Apr 29 '22 08:04 cns-solutions-admin

Sounds like a good idea, at high level -- I'll have think about details some more. But I agree that the ability to use external factory object/methods would be useful. It could be an extension of @JsonDeserialize as suggested, or possible a new annotation like @JsonFactory. I'd be happy to help if you or anyone wanted to try to implement this; I may not have much time in near future to work on it myself (but who knows), but always willing to help others get PRs through.

cowtowncoder avatar Apr 30 '22 03:04 cowtowncoder

On Thu, Jul 7, 2022 at 9:18 AM JulienElkaim @.***> wrote:

Hello,

I am not a huge fan of the current choice that we have to modify/annotate our classes to make the objects deserializable. I would love to be able to use something like this:

public class Item { // Fully POJO, no Jackson element on it private String name; private Integer ref; ... }

@JsonCreatorProvider public ItemFactory { @JsonCreator public static Item @.***("name") String name, @JsonProperty("ref") Integer ref) { ... } }

@cowtowncoder https://github.com/cowtowncoder , would you accept this concept if I submit such PR? Thinking about it now, I assume it would require reflexion to discover JsonCreator without the hint from a @JsonDeserialize annotation on the deserialize object's class.

Why not add this option to the ObjectMapper to scan a package for @JsonCreatorProvider, and while trying to deserialize an object if no creator is defined on the class, then look in cached JsonCreators discovered during the reflection phase at ObjectMapper initialization?

The only point I am afraid of is that the concept of making the ObjectMapper responsible to store and know how to deserialize does not fit with the current philosophy to make objects responsible to declare how they will be deserialized.

Good question; I am glad you asked!

I would not accept something that requires classpath scanning to auto-detect providers; Jackson does not use classpath scanning now and I don't want to add it (too fragile, too much black magic for core system; probably would also require dependency to a library that handles it).

But I would be fine with added extension points for registering such providers/factories, if necessary; someone can then create a module that uses whatever mechanism for auto-detection. So I am not against functionality itself, only inclusion within core.

Message ID: @.***>

cowtowncoder avatar Oct 11 '22 08:10 cowtowncoder