Skip to content

Unable to deserialize a previously serialized object - Pending Callbacks and Reference Resolvers #193

Description

@mressler

What is the expected and actual behavior?

Context.ts:65 is assuming that when context.pendingCallbacks == context.pendingRefsCount that all pending callbacks are pending reference resolvers. This is not the case.

It is possible for pendingCallbacks to be equivalent to pendingRefsCount when there are many more unresolved forward references. Here is a test case that fails:

it("(de)serializes class hierarchy with many forward references", () => {
        class EventState {
            @serializable(identifier())
            id: string

            @serializable
            name: string
        }

        class Event {
            @serializable(identifier())
            id: string

            @serializable
            name: string

            @serializable(reference(EventState))
            startingState: EventState

            @serializable(reference(EventState))
            endingState: EventState

            @serializable(list(reference(EventState)))
            relatedEvents: Event[]
        }

        class EventListing {
            @serializable(identifier())
            id: string

            @serializable(list(object(Event)))
            events: Event[]

            @serializable(list(object(EventState)))
            eventStates: EventState[]
        }

        const state1 = new EventState()
        state1.id = "state1"
        state1.name = "State 1"

        const state2 = new EventState()
        state2.id = "state2"
        state2.name = "State 2"

        const event1 = new Event()
        event1.id = "event1"
        event1.name = "Event 1"
        event1.startingState = state1
        event1.endingState = state2
        event1.relatedEvents = []

        const event2 = new Event()
        event2.id = "event2"
        event2.name = "Related Event 2"
        event1.relatedEvents.push(event2)

        const event3 = new Event()
        event3.id = "event3"
        event3.name = "Related Event 3"
        event1.relatedEvents.push(event3)

        const event4 = new Event()
        event4.id = "event4"
        event4.name = "Related Event 4"
        event1.relatedEvents.push(event4)

        const eventListing = new EventListing()
        eventListing.id = "listing1"
        eventListing.events = [event1, event2, event3, event4]
        eventListing.eventStates = [state1, state2]

        const serialized = serialize(eventListing)
        console.log("Serialized event listing:", JSON.stringify(serialized, null, 2))

        expect(serialized.id).toBe(eventListing.id)
        expect(serialized.events.length).toBe(4)
        expect(serialized.eventStates.length).toBe(2)
        expect(serialized.events[0].id).toBe(event1.id)
        expect(serialized.events[0].startingState).toBe(state1.id)
        expect(serialized.events[0].endingState).toBe(state2.id)
        expect(serialized.events[0].relatedEvents.length).toBe(3)
        expect(serialized.events[0].relatedEvents[0]).toBe(event2.id)
        expect(serialized.events[0].relatedEvents[1]).toBe(event3.id)
        expect(serialized.events[0].relatedEvents[2]).toBe(event4.id)

        const deserEventListing = deserialize(EventListing, serialized)
        expect(deserEventListing.id).toBe(eventListing.id)
        expect(deserEventListing.events.length).toBe(4)
        expect(deserEventListing.eventStates.length).toBe(2)
        expect(deserEventListing.events[0].id).toBe(event1.id)
        expect(deserEventListing.events[0].startingState.id).toBe(state1.id)
        expect(deserEventListing.events[0].endingState.id).toBe(state2.id)
        expect(deserEventListing.events[0].relatedEvents.length).toBe(3)
        expect(deserEventListing.events[1].id).toBe(event2.id)
        expect(deserEventListing.events[1].startingState.id).toBe(state1.id)
        expect(deserEventListing.events[1].endingState.id).toBe(state2.id)
    })

This produces the following output:

Serialized event listing: {
        "id": "listing1",
        "events": [
          {
            "id": "event1",
            "name": "Event 1",
            "startingState": "state1",
            "endingState": "state2",
            "relatedEvents": [
              "event2",
              "event3",
              "event4"
            ]
          },
          {
            "id": "event2",
            "name": "Related Event 2",
            "startingState": null,
            "endingState": null
          },
          {
            "id": "event3",
            "name": "Related Event 3",
            "startingState": null,
            "endingState": null
          },
          {
            "id": "event4",
            "name": "Related Event 4",
            "startingState": null,
            "endingState": null
          }
        ],
        "eventStates": [
          {
            "id": "state1",
            "name": "State 1"
          },
          {
            "id": "state2",
            "name": "State 2"
          }
        ]
      }

      at Object.<anonymous> (test/typescript/ts.test.ts:1059:17)

  ● @subSchema › (de)serialize class hierarchy with many forward references

    Error: Unresolvable references in json: "state1", "state2", "event2", "event3", "event4"

    

      at Context.GUARDED_NOOP [as onReadyCb] (src/utils/utils.ts:2067:11)
      at src/core/Context.ts:1767:20
      at src/utils/utils.ts:2093:14
      at onAfterDeserialize (src/core/deserialize.ts:2802:5)
      at src/core/deserialize.ts:2845:45
      at src/types/identifier.ts:527:9
      at Object.deserializer (src/types/primitive.ts:322:19)
      at Object.deserializer (src/types/identifier.ts:509:41)
      at deserializeProp (src/core/deserialize.ts:2837:13)
      at callbackDeserialize (src/core/deserialize.ts:2922:9)
      at onBeforeDeserialize (src/core/deserialize.ts:2777:5)
      at deserializePropsWithSchema (src/core/deserialize.ts:2931:37)
      at deserializeObjectWithSchema (src/core/deserialize.ts:2750:3)
      at Object.deserializer (src/types/object.ts:825:65)
      at callbackBefore (src/types/list.ts:1068:24)
      at onBeforeDeserialize (src/core/deserialize.ts:2777:5)
      at processItem (src/types/list.ts:1094:47)
      at src/utils/utils.ts:2138:5
          at Array.forEach (<anonymous>)
      at parallel (src/utils/utils.ts:2134:6)
      at Object.deserializer (src/types/list.ts:1098:28)
      at deserializeProp (src/core/deserialize.ts:2837:13)
      at callbackDeserialize (src/core/deserialize.ts:2922:9)
      at onBeforeDeserialize (src/core/deserialize.ts:2777:5)
      at deserializePropsWithSchema (src/core/deserialize.ts:2931:37)
      at deserializeObjectWithSchema (src/core/deserialize.ts:2750:3)
      at deserialize (src/core/deserialize.ts:2703:12)
      at Object.<anonymous> (test/typescript/ts.test.ts:1070:63)

I'm not sure what that line is guarding for. It seems like we should be able to decrement pendingCallbacks and only call the completion handler when pendingCallbacks == 0?

I'm playing around with this and will hopefully provide a PR. Happy to take direction.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions