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

Use model constructor when necessary (enables immutable models, encapsulation, Kotlin data classes etc) #213

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Alexander--
Copy link

@Alexander-- Alexander-- commented Jul 5, 2017

This pull request adds a fallback to non-default constructors for instantiating model class.

As of version 1.3.7 LoganSquare models are required to have either mutable public fields or public getter and setter:

@JsonObject
class Dto {
    @JsonField
    public String property1;

    @JsonField
    private int property2;

    public int getProperty2() { return property2; }

    public void setProperty2(int value) { this.property2 = value; }
}

This rubs many "enterprise specialists" wrong way and makes LoganSquare incompatible with certain use cases (such as Kotlin data classes). In comparison, various reflection-based JSON libraries have no issues with classes like this:

@JsonObject
class ImmutableDto {
    @JsonField
    private final property;

    public ImmutableDto(String property) {
        this.property = property;
    }
}

Those libraries typically use reflection to instantiate the class without calling constructor, then proceed to directly assign values to internal fields, (ab)using reflection conduct to disregard their private/final qualifiers.

Compared to those libraries, supporting non-default constructors in LoganSquare is slightly non-trivial due to it's compile-time nature.

In addition to using a constructor if assigning fields directly is impossible, this pull-request adds a check to forbid subclassing other models from models, that require constructor-based injection (subclassing in the opposite direction is harmless, so it is still allowed).

In addition to better encapsulation and increased safety,
this allows LoganSquare to support Kotlin data classes.

For each fields, marked for parsing from JSON, there must be a
matching constructor parameter in the corresponding position. The names
of parameters are not validated, instead the number and types of parameters
are relied on for safety.

Each model uses either setter/field injection (old behavior) or constructor
injection (new behavior); mixing setters and constructor parameters within
same model is not supported. Calling `parseField` on JsonMappers of models
with constructor injection produces UnsupportedOperationException.

The impact on existing code should be minimal — the new parsing strategy
gets used only when previous version of LoganSquare would produce an error:
e.g. if there are final final or private fields without setters.

See pull request for full description of feature
@Alexander-- Alexander-- force-pushed the feature-constructor-injection branch from 3f2f0b3 to e300421 Compare July 6, 2017 13:22
@mannodermaus
Copy link
Contributor

mannodermaus commented Jul 7, 2017

Thank you! This is great work that paves the way going forward in a lot of directions. Preventing subclassing of constructor-injected classes seems reasonable in order to prevent accidental mis-mappings of fields. If the constructor args need to be declared in order of appearance inside the class (kind of like AutoValue in a sense, I guess), are there type checks included already?

@JsonObject
class A {
    @JsonField
    final String field1;
    @JsonField
    final int field2;

    A(int field2, String field1) {
        // ...
    }
}

ERROR: Order of constructor parameters in `example.A` doesn't match declared fields! Expected `String field1` at position 0.

I suppose it's the case out-of-the-box, but would this be caught with an error at compile-time? Also, how about the already mentioned accidental incorrect ordering of fields with the same type. Maybe we could log a statement (either INFO or even WARNING) if the parameter order doesn't seem to match the corresponding fields?

@JsonObject
class A {
    @JsonField
    String field1;
    @JsonField
    String field2;

    A(String field2, String field1) {
        // ...
    }
}

WARNING: Order of constructor parameters in `example.A` possibly doesn't match declared fields. Expected `String field1` to be declared at position 0.

Maybe this last case isn't even necessary, I'm just trying to make sure we think of everything upfront when it comes to a change of this magnitude.

@Alexander--
Copy link
Author

@aurae

are there type checks included already?

No, there are none in this pull-request. The javac can do them better anyway. My main motivation is support for Kotlin data classes; multiple constructors with type ambiguity, varargs etc. do not matter there.

Also, how about the already mentioned accidental incorrect ordering of fields with the same type.

Rather than "incorrect ordering of fields", this is really "incorrect ordering of constructor parameters" (order of fields does not matter, as evident from widespread success of field injection).

This issue does not affect Kotlin data classes, so I haven't coded any solution yet. I agree, that warning is suitable (IntelliJ sometimes warns in case of label and variable name mismatch, so there is a good precedent for that already). A facility to suppress such warnings (via @SuppressWarnings) would have to be added at the same time.

Personally I don't see mis-ordering of constructor arguments as a deal-breaker. People can always shoot themselves in the foot with constructors like this:

DataClass(String property1, String property2) {
    this.property1 = property2;
    this.property2 = property1;
}

This is the price, that comes with writing constructors and imperative programming in general: the developer can screw it up. In absence of facility like Kotlin data classes, it is actually safer to use field-based injection, period.

@mannodermaus
Copy link
Contributor

Yeah, I thought the same when it comes to accidentally assigning the parameter to the wrong field. It shouldn't really matter because of that!

@EricKuck how do you feel about this contribution?

@Alexander--
Copy link
Author

@EricKuck It have been over two weeks already. I understand, that you might be busy with other stuff, but please consider having at least brief look at this submission.

Alternatively, if you don't really intend to incorporate this feature (or merge this particular pull-request), you can save me a lot of trouble by declining it.

Unless you have completely forgotten your way around guts of the library, in which case… it is time to fork! :D

@I60R
Copy link

I60R commented Sep 12, 2017

No activity for 2 years. It's definitely time to fork

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants