Skip to content

v1.1.2: "One-to-many (inversed)" not working as documented #345

Description

@heiwen

I'm trying to reproduce the following documented example:

import { Collection } from "@msw/data"
import z from "zod/v4"

const postSchema = z.object({
  get comments() {
    return z.array(commentSchema)
  },
})
const commentSchema = z.object({
  text: z.string(),
  get post() {
    return postSchema
  },
})

const posts = new Collection({ schema: postSchema })
const comments = new Collection({ schema: commentSchema })

posts.defineRelations(({ many }) => ({
  comments: many(comments),
}))
comments.defineRelations(({ one }) => ({
  post: one(posts),
}))

await posts.create({
  comments: [await comments.create({ text: 'First!' })],
})

const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
comments.post // { comments: [{ text: 'First', post: Circular }] }

It fails with the error message:

error: Failed to create a new record with initial values: does not match the schema. Please see the schema validation errors above.

I'm using Bun 1.2.0, msw 2.12.2, msw/data 1.1.2 and zod 4.1.12.

Activity

  1. changed the title [-]Data 1. 1.2: One-to-many (inversed) not working as documented[/-] [+]v1.1.2: "One-to-many (inversed)" not working as documented[/+] on Nov 17, 2025
  2. kettanaito commented on Nov 17, 2025

    @kettanaito
    Member

    Hi, @heiwen. Thanks for pointing this out.

    Since self-referencing non-optional records are currently impossible (see #318), the example has to be updated to create post through comments (comments can have an empty array as initial value, which won't violate the schema).

    This is what you should do instead:

    await comments.create({
      text: 'First!',
      post: await posts.create({ comments: [] })
    })

    Despite setting an empty comments upon creating the post, Data will push the created comment to that post's comments automatically. The empty array is there to align with the schema that doesn't mark either comments or post as optional.

    Could you please try this and let me know if this works in your case? Thanks.

  3. heiwen commented on Nov 18, 2025

    @heiwen
    Author

    Thank you for the quick reply, that works! :)

    I'm also able to add a second post by doing the following:

    const comment = comments.findFirst((q) => q.where({ text: 'First!' }))
    const post = comment!.post // { comments: [{ text: 'First', post: Circular }] }
    
    await comments.create({
      text: 'Second!',
      post: post
    })
    

    BUT, when returning the post or comment in msw using a HttpResponse.json, I now get the following error message:

    TypeError: Converting circular structure to JSON
        --> starting at object with constructor 'Object'
        |     property 'comments' -> object with constructor 'Array'
        |     index 0 -> object with constructor 'Object'
        --- property 'post' closes the circle
       at JSON.stringify (<anonymous>)
    

    I understand the nature of the error.

    Just wondering whether there is a recommended approach / is there a convenient way to remove the circular dependency when returning the data in the API mock without having to write a lot of boilerplate in every route?

  4. kettanaito commented on Nov 18, 2025

    @kettanaito
    Member

    Circular objects are hard. For JSON responses, I wonder if there's an unopinionated solution we can present. It is really up to you how you structure your models, but it would be great if the library helped with situations like this.

    What kind of solution do you see here?

    This reminds me of circular graphs in GraphQL, where they solve it by never exposing you the data you haven't explicitly requested. That's not a thing with RESTful APIs that usually return complete resources.

    On a technical level, someone has to handle the serialization of such circular objects at some point. But this is really a design question more than anything. Where should such circular references short-circuit? How would the user control that?

  5. heiwen commented on Nov 18, 2025

    @heiwen
    Author

    I'd probably ask: "How do other ORMs like Prisma / Drizzle" handle it?

    I think in their schema, one side of the relation is always defined as the default. In a query itself, you can query both sides But when data is returned, only one side is attached, avoiding circular dependencies.

    Is that conceptually a solution for msw data as well?

  6. kettanaito commented on Nov 18, 2025

    @kettanaito
    Member

    Would you be interested in help me with finding concrete examples from those libraries and presenting them here?

    Data is open to those solutions since it's modeled after libraries like Drizzle and Prisma. So, yes, we should learn from them and come up with an approach to help everyone in this situation. I'd be grateful for teaming up with you on this one.

  7. heiwen commented on Nov 18, 2025

    @heiwen
    Author

    Here is what I see from Prisma (which we use for our real API):

    It seems to be purely query based. Meaning, any property that represents a relation needs to be explicitly defined in the query so that it is included in the returned object (using an include parameter in the query):

    https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries#include-a-relation

    This is also true for create operations (which return the original object):

    https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries#create-a-related-record

    By nature of that explicit "include" definition, circular structures are avoided.

  8. heiwen commented on Apr 21, 2026

    @heiwen
    Author

    Flagging that the documentation is still mentioning the not working example:
    https://github.com/mswjs/data#one-to-many-inversed

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions