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

Rework inheritance #159

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

MatthiasKunnen
Copy link
Contributor

@MatthiasKunnen MatthiasKunnen commented Jan 19, 2021

Implemented in fork @Decoverto

This PR continues on top of #158 by reworking inheritance.

The PR removes knownTypes, typeHintEmitter, typeResolver, and nameResolver with a more comprehensive @jsonObjectInheritance decorator. The new implementation is also a lot less complex. 330 lines of source code get deleted 🚀.

Since this PR is branched from #158, the diff will include changes from that PR. For only the inheritance rework, see: MatthiasKunnen/TypedJSON@issue-139-define-type-lazily...MatthiasKunnen:rework-inheritance.

Todo:

  • Add documentation
  • Add more tests, any scenarios you would like to see tested?

@MatthiasKunnen MatthiasKunnen marked this pull request as draft January 19, 2021 10:52
@Neos3452
Copy link
Collaborator

I skimmed through it quickly, and I see two major points atm:

  1. I think you completely removed typeHintEmitter which creates a discriminator used by a service consuming TypedJSON serialisation output. Without it, there is no easy way to serialise abstract objects, or am I missing something?
  2. From experience when I had to use inheritance in serialisation it would take a form of a list of subclasses each having a predefined discriminator value. With the annotation you provided this seems pretty easy but would grow quickly in size with the amount of subclasses. This could be even better if one could provide a table of subclass -> discriminator value, and get a default serialisation out of the box, and allow to customise it completely with a callback like you did. Something like this https://www.baeldung.com/jackson-inheritance#2-per-class-annotations . What do you think?

@MatthiasKunnen
Copy link
Contributor Author

MatthiasKunnen commented Jan 21, 2021

  1. I removed it indeed. Why? Because I worked with services such as GraphQL which only allow data matching an exact schema. When TypedJSON adds a _type parameter this schema is no longer adhered to and the call will fail.
    About serialization: I do not see an issue here, do you have a specific example or test I could perform?

  2. I believe we should make our solutions as simple as possible, both for our sake and the user's sake. Ideally, users can understand and implement without reading documentation. To that end we should limit ourselves to the least amount of options required.
    The example you mentioned could be implemented as follows:

    @jsonObjectInheritance({
        resolveType: data => {
            const discriminators = {
                Cow: Cow,
                Dog: Dog,
                Cat: Cat,
            };
    
            return discriminators[data.type] ?? Animal;
        },
    })
    @jsonObject()
    class Animal {
    }

    I believe this approach would be more readable and easier to understand than adding the feature you described. It does not require more code on our end and does not leave the user thinking about how the resolveType function and discriminatorMatcher work together. What do you think?

@MatthiasKunnen
Copy link
Contributor Author

@JohnWeisz, I've appended this PR with discriminatorProperty which is used like this:

        @jsonInheritance(discriminatorProperty({
            property: 'type',
            types: () => ({
                Employee: Employee,
                Investor: Investor,
                PartTimeEmployee: PartTimeEmployee,
            }),
        }))
        @jsonObject
        class Person {
            name: string;
        }

This functionality enables the following flow: {name: "Bob", type: "PartTimeEmployee"} gets deserialized into ParTimeEmployee {name: 'bob'} and this gets serialized back into {name: "Bob", type: "PartTimeEmployee"}.

Does this approach work for your use case?

@MatthiasKunnen MatthiasKunnen force-pushed the rework-inheritance branch 3 times, most recently from eb5e533 to 41be7f5 Compare January 23, 2021 22:07
@MatthiasKunnen
Copy link
Contributor Author

I'm currently investigating some issues with circular references and multiple jsonInheritance decorators. I'll let you know when the PR is ready for review again.

Copy link
Collaborator

@Neos3452 Neos3452 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of points:

  1. Can we get the old tests to have the same exceptions, but with just the new decorator? I saw some were changed, and we would like to support the same use cases
  2. I saw some tests completely removed, why is that?
  3. We need some docs, and explanation for the circular dependency issue. Would be nice to link to the original blog post
  4. I think we will need to document an upgrade path too

/**
* Function to be used to mutate the resulting serialization given the source object.
*/
onSerializeType?: (source: any, result: {[k: string]: any}) => void;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of mutation can it return a value that should be used as a result? Does not change much, but allows to use pure functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (reverseMapping === undefined) {
const reverseMapItems: Array<[Function, string]> = Object.keys(types())
.map(discriminator => {
return [types()[discriminator].prototype.constructor, discriminator];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

types() is evaluated for each element of the dictionary. You could save it in a variable

Copy link
Contributor Author

@MatthiasKunnen MatthiasKunnen Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not make a difference IMO as I have to call a function regardless after the map is made, it is no longer called. I'll add a commit to show it. The result is less readable with minimal benefit IMO. I could save it in a variable and make it even less readable.

result[property] = getReverseMapping().get(source.constructor);
},
resolveType: data => {
return types()[data[property]];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is maybe a nit, but onSerializeType evaluates types once and stores the result, while here it will evaluated every time. It could mean the behaviour is not consistent across both

Copy link
Contributor Author

@MatthiasKunnen MatthiasKunnen Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here: Does not make a difference IMO as I have to call a function regardless so no gain. I'll add a commit to show it. The result is less readable with minimal benefit IMO.

@MatthiasKunnen MatthiasKunnen force-pushed the rework-inheritance branch 2 times, most recently from b992d10 to 836ec19 Compare January 27, 2021 15:19
@MatthiasKunnen
Copy link
Contributor Author

Rebase complete

@JohnWeisz JohnWeisz self-requested a review January 27, 2021 16:01
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.

2 participants