[Realm React] Schema and Embedded Array Confusion

Hi everyone, I’m very confused about how to define schemas when writing in TypeScript (specifically React Native), especially how to write schemas such that embedded objects and arrays work in a Realm database.

How Should I Define Schemas?

The Node.js SDK and React Native SDK say there are two options for defining a realm object model:

JavaScript objects

const Car = {
  name: "Car",
  properties: {
    _id: "objectId",
    make: "string",
    model: "string",
    miles: "int?",
  },
};

JavaScript Classes

class Car extends Realm.Object {
  static schema = {
    name: "Car",
    properties: {
      _id: { type: 'objectId', default: () => new Realm.BSON.ObjectId() },
      make: "string",
      model: "string",
      miles: "int?",
    },
    primaryKey: '_id',
  };
}
  • When should I define a Realm object using JavaScript classes/objects?
  • When do I need to define a static schema or type?
  • When should I use a generate function vs passing in an object to the Realm.create function?
  • How does one incorporate all of this with TypeScript?

Mapping Objects/Classes to Schemas

Then there’s a completely separate method of defining schemas, outlined in the Realm React documentation.

If one defines a Realm object like so:

type Label = {
  name: string;
  color: string;
}

export class Task extends Realm.Object<Task> {
  _id: Realm.BSON.UUID;
  userId!: string;
  name!: string;
  description: string;
  createdAt: Date = new Date();
  labels: Label[];
  static primaryKey = '_id';

How does this map to the schema defined in the Realm/Atlas schema UI, which uses JSON?

(Also note that, for me, Label[] does not work.)

Embedded Arrays

When working with embedded objects and arrays—trying to write non-primitives to my database—I run into a situation where the objects and arrays are simply ignored in the realm.write operation.

For example, suppose I have the following object:

const Task: Task = {
  name: 'Example Task',
  description: 'This is an example',
  createdAt: new Date(),
  author: {
    id: 'some_uuid',
    name: 'Author Name',
    authorIconUrl: 'some_url'
  },
  labels: [
     {
        'name': 'Important',
        'color': 'Red'
     },
     {
        'name': 'Family',
        'color': 'Gold'
     },
  ]
}

When I tried writing this in the past, the resultant “Task” in the database would contain everything except for the author and labels properties; it’s as though the realm.write function catches that author and labels are objects and ignores them without any warning/error message/feedback.

I changed my schema implementation (I honestly have no idea what I did, hence the questions about schema declarations above), and magically author and the embedded object showed up. However, the behavior with the array (realm.write quietly ignores it) still happens, and I don’t know how to solve this issue.

Please help! I think I have a barely functional knowledge of realm objects, classes, and schemas, especially in the context of TypeScript, and as such I have no idea how to write embedded objects and arrays of objects to Realm database documents.

  • How does one get embedded objects to work in React Native with TypeScript?
  • How does one write arrays of objects to documents in a Realm database in React Native with TypeScript?

TypeScript

Error:

Classes extending Realm.Object cannot define their own `schema` static, all properties must be defined using TypeScript syntax

Well… :frowning:

Hi @Alexander_Ye,

I’m sorry you’ve encountered so much resistance in getting your app working. We’re aware of these pain points in the docs are working to improve them.

Currently, we’re updating the React Native SDK to default to @realm/react and TypeScript. You can take a look at our progress in this PR. Keep in mind that this work is not complete and may change before it’s merged. We also haven’t gotten to all of the React Native SDK pages yet, so some pages in the staging site are still using the old guidance.

The updated Define a Realm Object Mode and Embedded Objects pages should be more helpful.

Please take a look and let me know if the newer docs help.

1 Like

The article you link “Define a Realm Object Model” recommends a syntax which will throw an error when used:
“Classes extending Realm.Object cannot define their own schema static, all properties must be defined using TypeScript syntax”

Hmm. @Brian_Luther, can you share more info about your app? I can’t reproduce the error. Though I do recall seeing it in the past. What version of realm and realm/react are you using?

Versions I’m using:
realm 11.3.1
@realm/react 0.4.1
@realm/babel-plugin 0.1.1
expo 47.0.12
react-native 0.70.5

Perhaps this has to do with using @realm/babel-plugin to transpile typescript classes into the JSON schema format? I may try removing the babel-plugin and using JSON schemas instead. Many of the docs are written using the JSON schema syntax so it’s hard to tell how to accomplish the same things using the typescript syntax, I think it might be easier to just use the JSON schema.

For example when including a relationship in a class schema defined in the frontend in development mode, I see the JSON schema syntax that the docs describe being generated on the backend in the app services UI. Meaning the type of the field is an ObjectId and there is a relationship definition, eg

{ "createdBy": { "ref": "#/relationship/mongodb-atlas/bolo-6/User", "foreignKey": "_id", "isList": false } }

But when I try to construct an instance in the frontend, it expects the entire related object as an argument rather than the ObjectId of the related object. If I pass just the ObjectId to the constructor, I get the following error:

Error: Exception in HostFunction: Missing value for property 'User.userId'

Where User.userId is another property on the referenced object, seeming to indicate that it wants to be passed the entire referenced object. (Note that this example is slightly confusing, I have an Atlas collection called User to store application data about users, and each document stores the equivalent Realm userId). I am able to construct an object with a relationship to another object only by passing the entire related object to the constructor.

I can’t figure out if the latter issue I described is related to the OP’s or not, but I seem to be hitting a dead-end because many of the docs describe things in terms of JSON syntax and the frontend will only allow me to use the new TypeScript syntax.

@Brian_Luther, I’m so sorry for the veeeery delayed response. I haven’t had much time to spend on the forums in the last month… and a half. :scream:

Are you still running into this issue? I promise I won’t disappear for another month and a half, but I don’t want to dig into this too much if you’ve already found a solution.

Some high-level things to consider, just in case:

  • The React Native SDK docs have been completely overhauled now. We don’t currently doc using the babel plugin because it’s still somewhat experimental. There are bugs and we don’t want to recommend a potentially frustrating dev experience. My hunch is that you ran into one of those bugs.
  • You mentioned the JSON schema syntax being in lots of places. Are you referring to the static schema property as opposed to using the babel plugin? Just making sure.
  • I think we could expand the CRUD docs to have more examples of creating complex objects. I’ll add a ticket for this.

Hey Kyle, no worries at all, thanks for getting back.

I’ll check out the new version of the docs, that could definitely be helpful. Removing the babel plugin seems like a good call to me, bouncing back and forth between the typescript syntax and the static schema syntax was confusing. Add on top of that a different syntax in Atlas app services - JSON schema as it seems to be called - and it’s difficult to figure out how to do what you’re trying to do, or even where to do so (on the front-end or in Atlas). Some information regarding relationships was written in the JSON schema syntax (eg the syntax you see in Atlas app services), which made it really unclear if that’s what I needed to use in the front-end. Can’t say exactly where I encountered that, some information seems to be spread between different SDKs (or was).

Anyways, the point of that was just to communicate that 3 similar and not clearly differentiated schema syntaxes was a stumbling point, so getting the Babel/TypeScript version out of the mix seems helpful.

I still have not been able to clarify “the way” to create an object in Realm with a relationship property if you could chime in on that, it might be helpful to have in the CRUD docs too. By relationship property I mean the one-to-one or one-to-many relationships (not an embedded object) described here.

  • To use the example in the docs, PetOwner has a a property { pet: 'Pet?' }. In order to Realm.create() a PetOwner, my experience has been that you need to pass the entire related Pet object in to the constructor. Is that the case? For example:
realm.create('PetOwner', {
  name: 'Alice',
  birthDate: new Date('1987-01-01'),
  pet: {
    _id: aBsonId,
    name: 'Spot',
    age: 7,
    animalType: 'Dog'
})
  • This seems weird to me, I would expect to be able to pass the ID - that is after all what is stored. I could not find anywhere in the docs that addresses it (that was a little while ago now). Not a huge issue though, as you can just go get the object from realm and pass it to create.
  • I haven’t tested what happens if you try a make a pet owner with pet data that doesn’t match the data in the database, maybe it will throw?
  • Just as a personal note, overall I’m leaning away from using relationships. It seems like the benefit is mostly convenience, since we’re not saving network requests by joining as we would with a remote database. And from what I can tell, relationships only exist in Realm, not Mongo/Atlas, so you wouldn’t be able to use it in network queries anyway. In particular the problem outlined here makes it so this convenience is leaking into the data model itself, which is less than ideal. I haven’t tried using a related object with a related object, but this would naturally come up in my app, and it seems like the brittleness will just increase…

This is an excellent point. If/when we add the babel plugin way to the docs, we’ll need to make sure we do so in a way that doesn’t confuse folks. I appreciate you sharing your experience! It should help us guide people better in the future.

We could also potentially add some information about how the JS client maps client schemas to the Atlas App Services JSON schema. Between the client SDK and App Services docs, “schema” can mean so many different things. We’re working on addressing this, but it’s a complicated issue. Generally, though, we now refer to client object “schemas” as object models in an attempt to disambiguate.

Anyway, about creating objects and defining relationships:

Most Realm operations happen locally. This means that what you see in Atlas App Services is not directly comparable to what your client has. For example, the differences in the client object model and App Services JSON schema.

So, you can’t pass only an ID when creating a relationship. Locally, all Realm objects are indeed Objects. Theoretically, the JS SDK team could create an API for creating relationships in this way, but that doesn’t exist right now.

When you create a relationship, like your example:

You’re creating a new Pet object in addition to establishing the relationship. When you instead query for an existing object and pass the object, you’re adding a new relationship to that existing object.

Regarding your last bullet point: relationships can be hard to mentally map. Historically, I’m not sure the docs have done a great job helping map them. I don’t think I have any additional advice at the moment, but I’m looking into what you’ve posted and I’ll try to make these docs clearer. Relationships were definitely a stumbling block for me when I started using Realm.

I really appreciate you taking the time to share your thoughts and experiences!

Just to clarify a bit further, let’s say we have two different Realm.create operations, seen here:

const existingPetId = new BSON.ObjectId("645512e5b73d72169ac61b8c")
const existingPet = realm.objectForPrimaryKey(existingPetId)
// existingPet: {
//   _id: '645512e5b73d72169ac61b8c',
//   name: 'Spot',
//   age: 7,
//   animalType: 'Dog'
// }

const create1 = realm.create('PetOwner', {
	name: 'Alice',
	birthDate: new Date('1987-01-01'),
	pet: existingPet
})

const create2 = realm.create('PetOwner', {
	name: 'Alice',
	birthDate: new Date('1987-01-01'),
	pet: {
		_id: existingPetId,
		name: 'Spot',
		age: 7,
		animalType: 'Dog'
	}
})
  • If I understand correctly, those operations are not equivalent.
  • This seems to indicate that Realm.create is using the object reference itself (rather than the property values) to determine whether to reference an existing Pet or create a new one. Is that correct?
  • Realm then doesn’t even store an _id as we see in the Atlas schema, but rather it literally holds a reference to the related Pet in the Pets collection.
  • In my example, would create2 throw an exception along the lines of “Cannot create an object with id “645512e5b73d72169ac61b8c”, an object with that _id already exists”? I can test this out myself, just thought I would ask to clarify the mental model of what’s happening.

Happy to share, seems like it can be helpful to have a newcomer/outsider perspective sometimes, and it’s good for me to clarify how this stuff is working so thanks for taking the time. On the schema topic, I personally would’ve found it helpful to have the different schema syntaxes explicitly addressed side-by-side in the docs, but you may be doing that now and the typescript syntax is gone anyway, so this might already be addressed for someone coming in now.

Agh, again I must apologize for time getting away from me. Sorry, @Brian_Luther, You’re correct in your assumptions in the previous post.

  • The const create1 and const create2 operations are indeed not identical - for two reasons: 1) As written, this code would create two entirely separate “Alice” PetOwner objects. 2) create1 is using the reference to the existing Pet object while create2 is attempting to create a brand new Pet object.
  • If you don’t define a primary key, Realm will not automatically create one. This is different than how Atlas treats its schemas. If you have a synced Realm, I believe your objects automatically get an _id field if you haven’t defined one because Atlas needs that unique identifier.
    • You can use embedded objects to avoid this. Embedded objects cannot have primary keys.
  • You do indeed get an error for the code as written. Something like: “Attempting to create an object of type ‘Pet’ with an existing primary key value ‘645512e5b73d72169ac61b8c’”.

I don’t know if it’s helpful, but here’s how I would write your sample code (tested), but with cars because that’s what I already have set up. :laughing:. And assuming you want to create two CarOwners who own the same Car.

class CarOwner extends Realm.Object<CarOwner> {
  _id!: BSON.ObjectId;
  name!: string;
  birthDate!: Date;
  car!: Car | null;
  
  static schema = {
    name: "CarOwner",
    properties: {
      _id: "objectId",
      name: "string",
      birthDate: "date",
      car: "Car",
    },
    primaryKey: "_id",
  };
}
  
class Car extends Realm.Object<Car> {
  _id!: BSON.ObjectId;
  make!: string;
  model!: string;
  miles!: number;
  
  static schema = {
    name: "Car",
    properties: {
      _id: "objectId",
      make: "string",
      model: "string",
      miles: "int",
    },
    primaryKey: "_id",
  };
}

// Open realm with your object models.
const realm = await Realm.open({
  schema: [Car, CarOwner],
});

const existingCarId = new BSON.ObjectId("645512e5b73d72169ac61b8c");
// Create car object with specific _id.
realm.write(() => {
  realm.create(Car, {
    _id: existingCarId,
    make: "Hyundai",
    model: "Accent",
    miles: 12000,
  });
});

const existingCar = realm.objectForPrimaryKey(Car, existingCarId);

// Do both creation ops in one write transaction. More efficient.
realm.write(() => {
  realm.create(CarOwner, {
    _id: new BSON.ObjectId(),
    name: "Leia",
    birthDate: new Date("1987-01-01"),
    car: existingCar,
  });

  realm.create(CarOwner, {
    _id: new BSON.ObjectId(),
    name: "Han",
    birthDate: new Date("1987-01-01"),
    car: existingCar,
  });
});

// Contains an array of the two new CarOwner objects
const carOwners = realm.objects(CarOwner);