ThomasAribart / json-schema-to-ts

Infer TS types from JSON schemas 📝
MIT License
1.44k stars 31 forks source link

Support for MongoDB ObjectId field #83

Closed gurumaxi closed 2 years ago

gurumaxi commented 2 years ago

I use json schemas to validate my mongodb collections, and convert them to typescript types with json-schema-to-ts. Everything works fantastic.

The problem I have is that mongodb saves its ID fields as ObjectId objects, not plain strings. With the help of the deserialize option I could easily map the value to the ObjectId type like this:

type Task = FromSchema<typeof TaskSchema, {
    deserialize: [{
        pattern: {
            type: 'string',
            description: 'objectId'
        },
        output: ObjectId,
    }],
}>;

The problem is that in order to be a valid validation schema, I cannot assign the type string to an id field. Mongodb forces all id fields to have the type bsonType: 'objectId' and to not be just simple string fields. Since the attribute bsonType is not part of the valid json schema definition, the FromSchema<> conversion obviously throws an error. Is there a way I can circumvent this problem? Maybe extending the JSONSchema type definitions?

I hope there is a solutions, since I really want to work with json-schema-to-ts because of its many advantages.

taxilian commented 2 years ago

Heh, I just hit this same issue =] Would love to see this!

taxilian commented 2 years ago

So here is one possibility -- I haven't tested it yet with mongodb but it would be easy to hack it a bit to make it work.

const ConsumerPerms = [
  'create',
  'read',
  'admin',
] as const;
type ConsumerPerms = typeof ConsumerPerms[number];
const consumerSchema = {
  type: "object",
  properties: {
    user: {
      type: "string",
      bsonType: 'objectId',
    },
    uniqueSlug: { type: "string" },
    name: { type: "string" },
    perms: {
      type: "array",
      items: { enum: ConsumerPerms },
    },
  },
  required: ["user", "uniqueSlug", "name", "perms"],
  additionalProperties: false,
} as const;

export type consumerFromSchema = FromSchema<typeof consumerSchema, {
  deserialize: [{
    pattern: {
      type: 'string',
      bsonType: 'objectId',
    },
    output: ObjectId,
  }],
}>;

The above seems to work perfectly with json-schema-to-ts -- it doesn't care if you add bsonType, you just still need to have type. Now what I don't know is if mongodb is going to puke if you define the above (with both bsonType and type) -- however, if it does then you could write a simple wrapper which would just recursively search through the schema object and remove the type field from anything which also has bsonType -- not the most elegant solution, but it would give you what you want without needing to wait for an update which allows customizing the type.

gurumaxi commented 2 years ago

If you define both type and bsonType it gives you the following error: Cannot specify both $jsonSchema keywords 'type' and 'bsonType'. For now I indeed wrote my own very minimal wrapper. It isn't the most beautiful way to solve this issue, but it works. I also needed to make sure that these objectId fields can also be nullable (for foreign references). So my deserialize options look like this:

{
    deserialize: [{
        pattern: {type: 'string', bsonType: 'objectId'},
        output: ObjectId,
    }, {
        pattern: {type: 'string', bsonType: ['objectId', 'null']},
        output: ObjectId,
    }, {
        pattern: {type: 'string', bsonType: ['null', 'objectId']},
        output: ObjectId,
    }],
}

and my wrapper method, which transforms my Json schema to bson:

function toBsonSchema(schema) {
    const bsonSchema = JSON.parse(JSON.stringify(schema));
    Object.values(bsonSchema.properties).forEach(obj => {
        if (obj.bsonType?.includes('objectId')) {
            delete obj.type;
        }
    });
    return bsonSchema;
}

I am not totally satisfied, but it works for the moment. But I would definitely appreciate if json-schema-to-ts would give as the possibility to specify bsonTypes.

ThomasAribart commented 2 years ago

Hi @gurumaxi and @taxilian,

The JSONSchema type constraint accepts custom keywords (like bsonType), but it throws if the schema has no property in common with JSONSchema. You don't need to have type, another keyword like examples would have worked :)

Anyway, this shouldn't be an issue since v2.5.5 which I just released to fix https://github.com/ThomasAribart/json-schema-to-ts/issues/84

Now the JSONSchema constraints has { [key: string]: unknown } in it, so { bsonType: "objectId" } should work just fine. Can you confirm this ?

ThomasAribart commented 2 years ago

@gurumaxi two notes:

You can use unions in the pattern keyword instead of using several patterns, it should work just fine:

type test = FromSchema<
  { bsonType: "objectId" },
  {
    deserialize: [
      {
        pattern: {
          bsonType: "objectId" | ["objectId", "null"] | ["null", "objectId"];
        };
        output: ObjectId;
      }
    ];
  }
>;
// => ObjectId

This is a bit better, but still doesn't scale very well. Sadly, I don't think you can check with an extend type ternary (which is how the deserialize feature works) that an array contains a certain value. But you can still use anyOf keyword instead of using an array in the type keyword:

type test =  FromSchema<
  { anyOf: [{ bsonType: "objectId" }, { bsonType: "somethingElse" }] },
  {
    deserialize: [
      {
        pattern: { bsonType: "objectId" };
        output: ObjectId;
      },
      {
        pattern: { bsonType: "somethingElse" };
        output: SomethingElse;
      }
    ];
  }
>;
// => ObjectId | SomethingElse

This will scale properly.

gurumaxi commented 2 years ago

Can confirm, the bsonType attribute now works without any JSONSchema property. Also, the union approach for the deserialize pattern is much better. Thank you very much!