schemars icon indicating copy to clipboard operation
schemars copied to clipboard

Self-referencing a type ends up creating a new definition

Open noomly opened this issue 2 years ago • 2 comments

For the following Rust enum:

#[derive(JsonSchema, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "function")]
pub enum Test {
    RandomVariantA(String),
    RandomVariantB(i32),
    RecursiveTests(Vec<Test>),
}

the following JSON schema is generated:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "Test",
    "oneOf": [
        {
            "type": ["object", "string"],
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["randomVariantA"]
                }
            }
        },
        {
            "type": ["object", "integer"],
            "format": "int32",
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["randomVariantB"]
                }
            }
        },
        {
            "type": ["object", "array"],
            "items": {
                "$ref": "#/definitions/Test" // <-------- reference to another extraneous `Test` definition
            },
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["recursiveTests"]
                }
            }
        }
    ],
    "definitions": {
        "Test": {
            "oneOf": [
                {
                    "type": ["object", "string"],
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["randomVariantA"]
                        }
                    }
                },
                {
                    "type": ["object", "integer"],
                    "format": "int32",
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["randomVariantB"]
                        }
                    }
                },
                {
                    "type": ["object", "array"],
                    "items": {
                        "$ref": "#/definitions/Test"
                    },
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["recursiveTests"]
                        }
                    }
                }
            ]
        }
    }
}

The issue here is that instead of using a self-reference for the variant RecursiveTests, Schemars creates another definition at #/definitions/Test. What I would expect would be this instead:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "Test",
    "oneOf": [
        {
            "type": ["object", "string"],
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["randomVariantA"]
                }
            }
        },
        {
            "type": ["object", "integer"],
            "format": "int32",
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["randomVariantB"]
                }
            }
        },
        {
            "type": ["object", "array"],
            "items": {
                "$ref": "#" // <------------------------- self reference
            },
            "required": ["function"],
            "properties": {
                "function": {
                    "type": "string",
                    "enum": ["recursiveTests"]
                }
            }
        }
    ]
}

Is there currently a way to achieve this with Schemars or is this a bug?

noomly avatar Oct 04 '22 09:10 noomly

I have experienced the same issue, but with types that have an indirect reference cycle instead (e.g. A references to B, which references to C, which references to A again), so using self references in the generated schema would not be enough in that case.

Maybe an expected output that also takes into account indirect reference cycles would be the following:

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "Test",
    "$ref": "#/definitions/Test", // <------------------------- reference to the definition
    "definitions": {
        "Test": {
            "oneOf": [
                {
                    "type": ["object", "string"],
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["randomVariantA"]
                        }
                    }
                },
                {
                    "type": ["object", "integer"],
                    "format": "int32",
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["randomVariantB"]
                        }
                    }
                },
                {
                    "type": ["object", "array"],
                    "items": {
                        "$ref": "#/definitions/Test"
                    },
                    "required": ["function"],
                    "properties": {
                        "function": {
                            "type": "string",
                            "enum": ["recursiveTests"]
                        }
                    }
                }
            ]
        }
    }
}

aeqz avatar Jun 10 '23 10:06 aeqz

Same thing here:


#[derive(JsonSchema)]
struct User {
	user_id: i32,
	first_name: String,
	last_name: String,
	role: Role,
	family: Vec<User>,
	gender: Gender,
}

Produces the following:


{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "User",
  "type": "object",
  "required": [
    "family",
    "first_name",
    "gender",
    "last_name",
    "role",
    "user_id"
  ],
  "properties": {
    "family": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/User"
      }
    },
    "first_name": {
      "type": "string"
    },
    "gender": {
      "$ref": "#/definitions/Gender"
    },
    "last_name": {
      "type": "string"
    },
    "role": {
      "$ref": "#/definitions/Role"
    },
    "user_id": {
      "type": "integer",
      "format": "int32"
    }
  },
  "definitions": {
    "Gender": {
      "type": "string",
      "enum": [
        "Male",
        "Female",
        "Other"
      ]
    },
    "Role": {
      "type": "string",
      "enum": [
        "Foo",
        "Admin"
      ]
    },
    "User": {
      "type": "object",
      "required": [
        "family",
        "first_name",
        "gender",
        "last_name",
        "role",
        "user_id"
      ],
      "properties": {
        "family": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/User"
          }
        },
        "first_name": {
          "type": "string"
        },
        "gender": {
          "$ref": "#/definitions/Gender"
        },
        "last_name": {
          "type": "string"
        },
        "role": {
          "$ref": "#/definitions/Role"
        },
        "user_id": {
          "type": "integer",
          "format": "int32"
        }
      }
    }
  }
}

Has anyone resolved this issue?

bweis avatar Nov 10 '23 20:11 bweis