Skip to content

OneOf vs AnyOf validators - java.lang.OutOfMemoryError: Java heap space #1162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
dmandalidis opened this issue Mar 29, 2025 · 6 comments
Closed

Comments

@dmandalidis
Copy link

dmandalidis commented Mar 29, 2025

Hi,

(version is 1.5.6)

We are facing an out-of-memory error while validating a nested data structure against a "one-of" restriction of a recursive schema.

In the attached definitions copy.json, if we change "one-of" to "any-of" in lines 147 and 187, the AnyOfValidator is being triggered instead and problem is not reproducible. However, doing that change on our side will lead to a looser schema and we want to avoid that.

In order to reproduce the problem, trying running the following unit test with -Xmx512m. Files being used are attached.

public class SchemaValidatorTest {

	private static final JsonMapper mapper = JsonMapper.builder()
			.findAndAddModules()
			.serializationInclusion(Include.NON_NULL)
			.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
			.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
			.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
			.build();
	
	private static final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
	
	@Test
	public void oom() throws Exception {
		try (InputStream schema = Thread.currentThread().getContextClassLoader().getResourceAsStream("definitions copy.json");
				InputStream data = Thread.currentThread().getContextClassLoader().getResourceAsStream("data copy.json")) {
			
			JsonSchema jsonSchema = factory.getSchema(schema);
			
			JsonNode dataTree = mapper.readTree(data);
			
			jsonSchema.validate(dataTree);
		}
	}
}

data copy.json
definitions copy.json

Setting the -Xmx lower to 256m leads to the following stacktrace if of any help:

java.lang.OutOfMemoryError: Java heap space
	at java.base/java.util.HashMap.resize(HashMap.java:702)
	at java.base/java.util.HashMap.putVal(HashMap.java:627)
	at java.base/java.util.HashMap.put(HashMap.java:610)
	at com.networknt.schema.PropertiesValidator.<init>(PropertiesValidator.java:53)
	at com.networknt.schema.ValidatorTypeCode$$Lambda$476/0x00007f1fb81b37e8.newInstance(Unknown Source)
	at com.networknt.schema.ValidatorTypeCode.newValidator(ValidatorTypeCode.java:162)
	at com.networknt.schema.JsonMetaSchema.newValidator(JsonMetaSchema.java:496)
	at com.networknt.schema.ValidationContext.newValidator(ValidationContext.java:63)
	at com.networknt.schema.JsonSchema.read(JsonSchema.java:542)
	at com.networknt.schema.JsonSchema.getValidators(JsonSchema.java:1316)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:612)
	at com.networknt.schema.RefValidator.validate(RefValidator.java:189)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.OneOfValidator.validate(OneOfValidator.java:98)
	at com.networknt.schema.OneOfValidator.validate(OneOfValidator.java:64)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.ItemsValidator202012.validate(ItemsValidator202012.java:79)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.PropertiesValidator.validate(PropertiesValidator.java:84)
	at com.networknt.schema.PropertiesValidator.validate(PropertiesValidator.java:61)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.RefValidator.validate(RefValidator.java:189)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.OneOfValidator.validate(OneOfValidator.java:98)
	at com.networknt.schema.OneOfValidator.validate(OneOfValidator.java:64)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.ItemsValidator202012.validate(ItemsValidator202012.java:79)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.PropertiesValidator.validate(PropertiesValidator.java:84)
	at com.networknt.schema.PropertiesValidator.validate(PropertiesValidator.java:61)
	at com.networknt.schema.JsonSchema.validate(JsonSchema.java:616)
	at com.networknt.schema.RefValidator.validate(RefValidator.java:189)

edit: more information

@dmandalidis
Copy link
Author

dmandalidis commented Mar 29, 2025

It is the cacheRefs; If I change the unit test above and use cacheRefs(false) it works.

I think it's a viable solution given that my $refs are always local in my case.

edit: clarification

@dmandalidis
Copy link
Author

It appears that cacheRefs makes the situation a little bit better but not resolves the problem :/

@stevehu
Copy link
Contributor

stevehu commented Apr 3, 2025

Recursive schema is always a problem as we are loading the schema in memory for validation. Is there a way to imporve the schema design?

@dmandalidis
Copy link
Author

Hi @stevehu ,

Yes I understand the complexity of this, and we might have hit a very reasonable limit. Just mentioned here in case this was something that may have slipped during loading the refs. Feel free to close this issue if this was something totally expected.

As a gut feeling, given a recursive schema, I think there's room for a different data structure for keeping it in memory, but I don't know if it is worthy it since you will be just pushing the limit further.

Thanks

@justin-tay
Copy link
Contributor

This is poor schema design. You will get poor performance regardless of whether there is enough memory as oneOf has no choice but to evaluate all the possibilities to its leaf. You wouldn't write a recursive function like this, given that you have a discriminator $type and can return earlier by checking this value instead of going to the leaf. You would also likely get confusing error messages using oneOf. Unfortunately there's currently no simple keyword in the standard that does this yet although one has already been proposed.

Your oneOf will need to be a allOf with if conditions to test the $type. You would also need a condition to constrain the $type using enum.

{
  "allOf": [
    {
      "if": {
        "properties": {
          "$type": { "const": "is" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-is" }
    },
    {
      "if": {
        "properties": {
          "$type": { "const": "contains" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-contains" }
    },
    {
      "if": {
        "properties": {
          "$type": { "const": "and" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-and" }
    },
    {
      "if": {
        "properties": {
          "$type": { "const": "or" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-or" }
    },
    {
      "if": {
        "properties": {
          "$type": { "const": "true" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-true" }
    },
    {
      "if": {
        "properties": {
          "$type": { "const": "one-of" }
        },
        "required": ["$type"]
      },
      "then": { "$ref": "#/$defs/rule-oneOf" }
    },
    {
      "properties": {
        "$type": { "enum": ["is","contains","and","or","true","one-of"] }
      },
      "required": ["$type"]
    }
  ]
}

Example

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "http://echa.europa.eu/dynamic_forms/schemas/config/definitions",
  "type": "object",
  "properties": {
    "outcomeRules": {
      "type": "array",
      "title": "Rules calculating the outcome",
      "items": {
        "$ref": "#/$defs/factRule"
      }
    }
  },
  "required": [
    "outcomeRules"
  ],
  "$defs": {
    "factRule": {
      "type": "object",
      "properties": {
        "fact": {
          "type": "string",
          "minLength": 1
        },
        "description": {
          "type": "string",
          "minLength": 1
        },
        "condition": {
          "$ref": "#/$defs/ruleCondition"
        }
      },
      "required": [
        "fact",
        "condition"
      ]
    },
    "ruleCondition": {
      "allOf": [
        {
          "if": {
            "properties": {
              "$type": {
                "const": "is"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-is"
          }
        },
        {
          "if": {
            "properties": {
              "$type": {
                "const": "contains"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-contains"
          }
        },
        {
          "if": {
            "properties": {
              "$type": {
                "const": "and"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-and"
          }
        },
        {
          "if": {
            "properties": {
              "$type": {
                "const": "or"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-or"
          }
        },
        {
          "if": {
            "properties": {
              "$type": {
                "const": "true"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-true"
          }
        },
        {
          "if": {
            "properties": {
              "$type": {
                "const": "one-of"
              }
            },
            "required": [
              "$type"
            ]
          },
          "then": {
            "$ref": "#/$defs/rule-oneOf"
          }
        },
        {
          "properties": {
            "$type": {
              "enum": [
                "is",
                "contains",
                "and",
                "or",
                "true",
                "one-of"
              ]
            }
          },
          "required": [
            "$type"
          ]
        }
      ]
    },
    "rule-contains": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "contains"
        },
        "field": {
          "type": "string",
          "minLength": 1
        },
        "value": {
          "oneOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "boolean"
            },
            {
              "type": "integer"
            }
          ]
        },
        "not": {
          "type": "boolean"
        }
      },
      "additionalProperties": false,
      "required": [
        "$type",
        "field",
        "value"
      ]
    },
    "rule-is": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "is"
        },
        "field": {
          "type": "string",
          "minLength": 1
        },
        "not": {
          "type": "boolean"
        },
        "value": {
          "oneOf": [
            {
              "type": "string",
              "minLength": 1
            },
            {
              "type": "boolean"
            },
            {
              "type": "integer"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "additionalProperties": false,
      "required": [
        "$type",
        "field"
      ]
    },
    "rule-and": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "and"
        },
        "not": {
          "type": "boolean"
        },
        "members": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": {
            "allOf": [
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "is"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-is"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "contains"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-contains"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "and"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-and"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "or"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-or"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "one-of"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-oneOf"
                }
              },
              {
                "properties": {
                  "$type": {
                    "enum": [
                      "is",
                      "contains",
                      "and",
                      "or",
                      "one-of"
                    ]
                  }
                },
                "required": [
                  "$type"
                ]
              }
            ]
          }
        }
      },
      "additionalProperties": false,
      "required": [
        "$type",
        "members"
      ]
    },
    "rule-or": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "or"
        },
        "not": {
          "type": "boolean"
        },
        "members": {
          "type": "array",
          "minItems": 2,
          "maxItems": 2,
          "items": {
            "allOf": [
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "is"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-is"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "contains"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-contains"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "and"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-and"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "or"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-or"
                }
              },
              {
                "if": {
                  "properties": {
                    "$type": {
                      "const": "one-of"
                    }
                  },
                  "required": [
                    "$type"
                  ]
                },
                "then": {
                  "$ref": "#/$defs/rule-oneOf"
                }
              },
              {
                "properties": {
                  "$type": {
                    "enum": [
                      "is",
                      "contains",
                      "and",
                      "or",
                      "one-of"
                    ]
                  }
                },
                "required": [
                  "$type"
                ]
              }
            ]
          }
        }
      },
      "additionalProperties": false,
      "required": [
        "$type",
        "members"
      ]
    },
    "rule-true": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "true"
        },
        "not": {
          "type": "boolean"
        }
      },
      "additionalProperties": false,
      "required": [
        "$type"
      ]
    },
    "rule-oneOf": {
      "type": "object",
      "properties": {
        "$type": {
          "const": "one-of"
        },
        "field": {
          "type": "string",
          "minLength": 1
        },
        "not": {
          "type": "boolean"
        },
        "values": {
          "type": "array",
          "items": {
            "oneOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "boolean"
              },
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "additionalProperties": false,
      "required": [
        "$type",
        "field",
        "values"
      ]
    }
  }
}

@dmandalidis
Copy link
Author

Hi @justin-tay ,

Thanks a million for this, this is a far better version of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants