Mensagens de erro aprimoradas para validação de esquema no MongoDB 5.0
Avalie esse anúncio
Muitos MongoDB dependem da validação de esquema para impor as regras que regem a estrutura e a integridade dos documentos em suas collection. Mas um dos desafios que eles enfrentaram foi entender rapidamente por que um documento que não correspondia ao esquema não podia ser inserido ou atualizado. Isso mudará no próximo MongoDB 5.0 versão.
A facilidade de uso da validação do esquema será significativamente melhorada com a geração de mensagens de erro descritivas sempre que uma operação falhar na validação. Essas informações adicionais fornecem uma visão valiosa sobre quais partes de um documento em uma operação de inserção/atualização falharam na validação em relação a quais partes do validador de uma collection e como. A partir dessas informações, você pode identificar e corrigir rapidamente erros de código que estão fazendo com que os documentos não estejam em conformidade com suas regras de validação. Chega de depuração tediosa cortando seu documento em pedaços para isolar o problema!
Se você quiser avaliar esse recurso e nos fornecer um feedback antecipado, preencha este formulário para participar do programa de pré-visualização.
A maneira mais popular de Express as regras de validação é JSON schema. É um padrão amplamente aceito que também é usado na especificação e validação da REST API. E no MongoDB, você pode combinar o JSON schema com a linguagem de query do MongoDB (MQL) para fazer ainda mais.
Nesta publicação, gostaria de revisar alguns exemplos para reiterar os recursos da validação de esquema e mostrar a adição de novas mensagens de erro detalhadas.
Primeiro, vamos dar uma olhada na nova mensagem de erro. É uma mensagem estruturada no formato BSON, explicando qual parte do documento não correspondeu às regras e qual regra de validação causou isso.
Considere este validador básico que garante que o campo de preço não aceite valores negativos. No JSON schema, a propriedade é equivalente ao que chamamos de "campo" no MongoDB.
1 { 2 "$jsonSchema": { 3 "properties": { 4 "price": { 5 "minimum": 0 6 } 7 } 8 } 9 }
Ao tentar inserir um documento com
{price: -2}
, a seguinte mensagem de erro será retornada.1 { 2 "code": 121, 3 "errmsg": "Document failed validation", 4 "errInfo": { 5 "failingDocumentId": ObjectId("5fe0eb9642c10f01eeca66a9"), 6 "details": { 7 "operatorName": "$jsonSchema", 8 "schemaRulesNotSatisfied": [ 9 { 10 "operatorName": "properties", 11 "propertiesNotSatisfied": [ 12 { 13 "propertyName": "price", 14 "details": [ 15 { 16 "operatorName": "minimum", 17 "specifiedAs": { 18 "minimum": 0 19 }, 20 "reason": "comparison failed", 21 "consideredValue": -2 22 } 23 ] 24 } 25 ] 26 } 27 ] 28 } 29 } 30 }
Alguns dos campos principais da resposta são:
failingDocumentId
- o _id do documento que foi avaliadooperatorName
- o operador usado na regra de validaçãopropertiesNotSatisfied
- a lista de campos (properties) que falharam nas verificações de validaçãopropertyName
- o campo do documento que foi avaliadospecifiedAs
- a regra como foi expressa no validadorreason - explanation
de como a regra não foi satisfeitaconsideredValue
- valor do campo no documento que foi avaliado
O erro pode incluir mais campos dependendo da regra de validação específica, mas essas são as mais comuns. Você provavelmente achará
propertyName
e reason
os campos mais úteis na resposta.Agora podemos examinar os exemplos das diferentes regras de validação e ver como a nova mensagem detalhada nos ajuda a identificar o motivo da falha na validação.
Como exemplo, usaremos uma coleção de propriedades Real Estate em Nova York gerenciadas por uma equipe de Agentes Real Estate .
Aqui está um documento de amostra:
1 { 2 "PID": "EV10010A1", 3 "agents": [ { "name": "Ana Blake", "email": "anab@rcgk.com" } ], 4 "description": "Spacious 2BR apartment", 5 "localization": { "description_es": "Espacioso apartamento de 2 dormitorios" }, 6 "type": "Residential", 7 "address": { 8 "street1": "235 E 22nd St", 9 "street2": "Apt 42", 10 "city": "New York", 11 "state": "NY", 12 "zip": "10010" 13 }, 14 "originalPrice": 990000, 15 "discountedPrice": 980000, 16 "geoLocation": [ -73.9826509, 40.737499 ], 17 "listedDate": "Wed Dec 11 2020 10:05:10 GMT-0500 (EST)", 18 "saleDate": "Wed Dec 21 2020 12:00:04 GMT-0500 (EST)", 19 "saleDetails": { 20 "price": 970000, 21 "buyer": { "id": "24434" }, 22 "bids": [ 23 { 24 "price": 950000, 25 "winner": false, 26 "bidder": { 27 "id": "24432", 28 "name": "Sam James", 29 "contact": { "email": "sjames@gmail.com" } 30 } 31 }, 32 { 33 "price": 970000, 34 "winner": true, 35 "bidder": { 36 "id": "24434", 37 "name": "Joana Miles", 38 "contact": { "email": "jm@gmail.com" } 39 } 40 } 41 ] 42 } 43 }
Nossas propriedades imobiliárias são identificadas com o ID da propriedade (PID) que deve seguir um formato de nomenclatura específico: ele deve começar com duas letras seguidas de cinco dígitos e algumas letras e dígitos depois, assim: WS10011FG4 ou EV10010A1.
Podemos usar o operador JSON Schema
pattern
para criar uma regra para isso como uma expressão regular.Validator:
1 { 2 "$jsonSchema": { 3 "properties": { 4 "PID": { 5 "bsonType": "string", 6 "pattern": "^[A-Z]{2}[0-9]{5}[A-Z]+[0-9]+$" 7 } 8 } 9 } 10 }
Se tentarmos inserir um documento com um campo PID que não corresponda ao padrão, por exemplo
{ PID: "apt1" }
, receberemos um erro.O erro informa que o campo
PID
tinha o valor de "apt1"
e não corresponde à expressão regular, que foi especificada como "^[A-Z]{2}[0-9]{5}[A-Z]+[0-9]+$"
.1 { ... 2 "schemaRulesNotSatisfied": [ 3 { 4 "operatorName": "properties", 5 "propertiesNotSatisfied": [ 6 { 7 "propertyName": "PID", 8 "details": [ 9 { 10 "operatorName": "pattern", 11 "specifiedAs": { 12 "pattern": "^[A-Z]{2}[0-9]{5}[A-Z]+[0-9]+$" 13 }, 14 "reason": "regular expression did not match", 15 "consideredValue": "apt1" 16 } 17 ] 18 } 19 ] 20 ... 21 }
A descrição pode estar localizada em vários idiomas. Atualmente, nosso aplicativo oferece suporte apenas a espanhol, alemão e francês, portanto, o objeto de localização só pode conter os campos
description_es
, description_de
ou description_fr
. Outros campos não serão permitidos.Podemos usar o operador
patternProperties
para descrever esse requisito como expressão regular e indicar que nenhum outro campo é esperado aqui com "additionalProperties": false
.Validator:
1 { 2 "$jsonSchema": { 3 "properties": { 4 "PID": {...}, 5 "localization": { 6 "additionalProperties": false, 7 "patternProperties": { 8 "^description_(es|de|fr)+$": { 9 "bsonType": "string" 10 } 11 } 12 } 13 } 14 } 15 }
Documentos como este podem ser inseridos com sucesso:
1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "localization": { 5 "description_es": "Amplio apartamento de 2 dormitorios", 6 "description_de": "Geräumige 2-Zimmer-Wohnung", 7 } 8 }
Documentos como este falharão na verificação de validação:
1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "localization": { 5 "description_cz": "Prostorný byt 2 + kk" 6 } 7 }
O erro abaixo indica que o campo
localization
contém propriedades adicionais description_cz
. description_cz
não corresponde ao padrão esperado, portanto, é considerada uma propriedade adicional.1 { ... 2 "propertiesNotSatisfied": [ 3 { 4 "propertyName": "localization", 5 "details": [ 6 { 7 "operatorName": "additionalProperties", 8 "specifiedAs": { 9 "additionalProperties": false 10 }, 11 "additionalProperties": [ 12 "description_cz" 13 ] 14 } 15 ] 16 } 17 ] 18 ... 19 }
Cada propriedade imobiliária em nossa collection tem um tipo, e queremos usar um dos quatro tipos: "Residencial," " Comercial," " Industrial," ou "Terreno." Isso pode ser feito com o operador
enum
.Validator:
1 { 2 "$jsonSchema": { 3 "properties": { 4 "type": { 5 "enum": [ "Residential", "Commercial", "Industrial", "Land" ] 6 } 7 } 8 } 9 }
O seguinte documento será considerado inválido:
1 { 2 "PID": "TS10018A1", "type": "House" 3 }
O erro informa que o campo
type
falhou na validação porque "o valor não foi encontrado no enum."1 {... 2 "propertiesNotSatisfied": [ 3 { 4 "propertyName": "type", 5 "details": [ 6 { 7 "operatorName": "enum", 8 "specifiedAs": { 9 "enum": [ 10 "Residential", 11 "Commercial", 12 "Industrial", 13 "Land" 14 ] 15 }, 16 "reason": "value was not found in enum", 17 "consideredValue": "House" 18 } 19 ] 20 } 21 ] 22 ... 23 }
Os agentes que gerenciam cada propriedade imobiliária são armazenados na matriz
agents
. Vamos nos certificar de que não haja elementos duplicados na matriz e que não haja mais de três agentes trabalhando com a mesma propriedade. Podemos usaruniqueItems
e maxItems
para isso.1 { 2 "$jsonSchema": { 3 "properties": { 4 "agents": { 5 "bsonType": "array", 6 "uniqueItems": true, 7 "maxItems": 3 8 } 9 } 10 } 11 }
O documento a seguir viola ambas as regras de validação.
1 { 2 "PID": "TS10018A1", 3 "agents": [ 4 { "name": "Ana Blake" }, 5 { "name": "Felix Morin" }, 6 { "name": "Dilan Adams" }, 7 { "name": "Ana Blake" } 8 ] 9 }
O erro retorna informações sobre a falha de duas regras: "array não corresponde ao comprimento especificado" e "encontrou um item duplicado," e também indica qual valor era duplicado.
1 { 2 ... 3 "propertiesNotSatisfied": [ 4 { 5 "propertyName": "agents", 6 "details": [ 7 { 8 "operatorName": "maxItems", 9 "specifiedAs": { "maxItems": 3 }, 10 "reason": "array did not match specified length", 11 "consideredValue": [ 12 { "name": "Ana Blake" }, 13 { "name": "Felix Morin" }, 14 { "name": "Dilan Adams" }, 15 { "name": "Ana Blake" } 16 ] 17 }, 18 { 19 "operatorName": "uniqueItems", 20 "specifiedAs": { "uniqueItems": true }, 21 "reason": "found a duplicate item", 22 "consideredValue": [ 23 { "name": "Ana Blake" }, 24 { "name": "Felix Morin" }, 25 { "name": "Dilan Adams" }, 26 { "name": "Ana Blake" } 27 ], 28 "duplicatedValue": { "name": "Ana Blake" } 29 } 30 ] 31 ... 32 }
Agora queremos garantir que haja informações de contato disponíveis para os agentes. Precisamos do nome de cada agente e pelo menos uma maneira de contatá-los: telefone ou e-mail. Usaremos
required
e anyOf
para criar esta regra.Validator:
1 { 2 "$jsonSchema": { 3 "properties": { 4 "agents": { 5 "bsonType": "array", 6 "uniqueItems": true, 7 "maxItems": 3, 8 "items": { 9 "bsonType": "object", 10 "required": [ "name" ], 11 "anyOf": [ { "required": [ "phone" ] }, { "required": [ "email" ] } ] 12 } 13 } 14 } 15 } 16 }
O documento a seguir falhará na validação:
1 { 2 "PID": "TS10018A1", 3 "agents": [ 4 { "name": "Ana Blake", "email": "anab@rcgk.com" }, 5 { "name": "Felix Morin", "phone": "+12019878749" }, 6 { "name": "Dilan Adams" } 7 ] 8 }
Aqui, o erro indica que o terceiro elemento da array (
"itemIndex": 2
) não corresponde à regra.1 { 2 ... 3 "propertiesNotSatisfied": [ 4 { 5 "propertyName": "agents", 6 "details": [ 7 { 8 "operatorName": "items", 9 "reason": "At least one item did not match the sub-schema", 10 "itemIndex": 2, 11 "details": [ 12 { 13 "operatorName": "anyOf", 14 "schemasNotSatisfied": [ 15 { 16 "index": 0, 17 "details": [ 18 { 19 "operatorName": "required", 20 "specifiedAs": { "required": [ "phone" ] }, 21 "missingProperties": [ "phone" ] 22 } 23 ] 24 }, 25 { 26 "index": 1, 27 "details": [ 28 { 29 "operatorName": "required", 30 "specifiedAs": { "required": [ "email" ] }, 31 "missingProperties": [ "email" ] 32 } 33 ] 34 } 35 ] 36 } 37 ] 38 } 39 ] 40 } 41 ] 42 ... 43 }
Vamos criar outra regra para garantir que, se o documento contiver o campo
saleDate
, saleDetails
também esteja presente e vice-versa: se houver saleDetails
,saleDate
também deverá existir.1 { 2 "$jsonSchema": { 3 "dependencies": { 4 "saleDate": [ "saleDetails"], 5 "saleDetails": [ "saleDate"] 6 } 7 } 8 }
Agora, vamos tentar inserir o documento com
saleDate
, mas sem saleDetails
:1 { 2 "PID": "TS10018A1", 3 "saleDate": Date("2020-05-01T04:00:00.000Z") 4 }
O erro agora inclui a propriedade com dependência
saleDate
e uma propriedade ausente nas dependências: saleDetails
.1 { 2 ... 3 "details": { 4 "operatorName": "$jsonSchema", 5 "schemaRulesNotSatisfied": [ 6 { 7 "operatorName": "dependencies", 8 "failingDependencies": [ 9 { 10 "conditionalProperty": "saleDate", 11 "missingProperties": [ "saleDetails" ] 12 } 13 ] 14 } 15 ] 16 } 17 ... 18 }
Observe que, no JSON schema, o campo
dependencies
está no objeto raiz, e não dentro da propriedade específica. Portanto, na mensagem de erro, o objetodetails
terá uma estrutura diferente:1 { "operatorName": "dependencies", "failingDependencies": [...]}
Nos exemplos anteriores, quando a regra do JSON schema estava dentro do objeto "properties", assim:
1 "$jsonSchema": { "properties": { "price": { "minimum": 0 } } }
os detalhes da mensagem de erro continham
"operatorName": "properties"
e um "propertyName"
:1 { "operatorName": "properties", 2 "propertiesNotSatisfied": [ { "propertyName": "...", "details": [] } ] 3 }
Você pode usar a Linguagem de Query do MongoDB (MQL) em seu validador ao lado do JSON schema para adicionar uma lógica de negócios mais rica às suas regras.
Como um exemplo, você pode usar $expr para adicionar um check-in para que um
discountPrice
seja menor que originalPrice
assim:1 { 2 "$expr": { 3 "$lt": [ "$discountedPrice", "$originalPrice" ] 4 }, 5 "$jsonSchema": {...} 6 }
$expr resolve para
true
ou false
e permite a você utilizar expressões de agregação para criar regras de negócio sofisticadas.Para um exemplo um pouco mais complexo, digamos que mantemos uma matriz de lances no documento de cada propriedade imobiliária, e o campo booleano
isWinner
indica se um lance específico é o vencedor.Documento de amostra:
1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "saleDetails": { 5 "bids": [ 6 { 7 "price": 500000, 8 "isWinner": false, 9 "bidder": {...} 10 }, 11 { 12 "price": 530000, 13 "isWinner": true, 14 "bidder": {...} 15 } 16 ] 17 } 18 }
Vamos nos certificar de que apenas um dos elementos da matriz
bids
possa ser marcado como vencedor. O validador terá uma expressão em que aplicamos um filtro à matriz de lances para manter apenas os elementos com"isWinner":
verdadeiro e verificar se o tamanho da matriz resultante é menor ou igual a 1.Validator:
1 { 2 "$and": [ 3 { 4 "$expr": { 5 "$lte": [ 6 { 7 "$size": { 8 "$filter": { 9 "input": "$saleDetails.bids.isWinner", 10 "cond": "$$this" 11 } 12 } 13 }, 14 1 15 ] 16 } 17 }, 18 { 19 "$expr": {...} 20 }, 21 { 22 "$jsonSchema": {...} 23 } 24 ] 25 }
Vamos tentar inserir o documento com poucos lances com
"isWinner": true
.1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "originalPrice": 600000, 5 "discountedPrice": 550000, 6 "saleDetails": { 7 "bids": [ 8 { "price": 500000, "isWinner": true }, 9 { "price": 530000, "isWinner": true } 10 ] 11 } 12 }
A mensagem de erro produzida indicará qual expressão foi avaliada como falsa.
1 { 2 ... 3 "details": { 4 "operatorName": "$expr", 5 "specifiedAs": { 6 "$expr": { 7 "$lte": [ 8 { 9 "$size": { 10 "$filter": { 11 "input": "$saleDetails.bids.isWinner", 12 "cond": "$$this" 13 } 14 } 15 }, 16 1 17 ] 18 } 19 }, 20 "reason": "expression did not match", 21 "expressionResult": false 22 } 23 ... 24 }
Como último exemplo, vejamos como podemos usar as funcionalidades geoespaciais do MQL para garantir que todas as propriedades Real Estate na collection estejam localizadas dentro dos limites da cidade de Nova York. Nossos documentos incluem um campo
geoLocation
com coordenadas. Podemos utilizar $geoWithin
para verificar se estas coordenadas estão dentro do polígono geoJSON (o polígono para a cidade de Nova York neste exemplo é aproximado).Validator:
1 { 2 "geoLocation": { 3 "$geoWithin": { 4 "$geometry": { 5 "type": "Polygon", 6 "coordinates": [ 7 [ [ -73.91326904296874, 40.91091803848203 ], 8 [ -74.01626586914062, 40.75297891717686 ], 9 [ -74.05677795410156, 40.65563874006115 ], 10 [ -74.08561706542969, 40.65199222800328 ], 11 [ -74.14329528808594, 40.64417760251725 ], 12 [ -74.18724060058594, 40.643656594948524 ], 13 [ -74.234619140625, 40.556591288249905 ], 14 [ -74.26345825195312, 40.513277131087484 ], 15 [ -74.2510986328125, 40.49500373230525 ], 16 [ -73.94691467285156, 40.543026009954986 ], 17 [ -73.740234375, 40.589449604232975 ], 18 [ -73.71826171874999, 40.820045086716505 ], 19 [ -73.78829956054686, 40.8870435151357 ], 20 [ -73.91326904296874, 40.91091803848203 ] ] 21 ] 22 } 23 } 24 }, 25 "$jsonSchema": {...} 26 }
Um documento como este será inserido com sucesso.
1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "geoLocation": [ -73.9826509, 40.737499 ], 5 "originalPrice": 600000, 6 "discountedPrice": 550000, 7 "saleDetails": {...} 8 }
O documento a seguir falhará.
1 { 2 "PID": "TS10018A1", 3 "type": "Residential", 4 "geoLocation": [ -73.9826509, 80.737499 ], 5 "originalPrice": 600000, 6 "discountedPrice": 550000, 7 "saleDetails": {...} 8 }
O erro indicará que a validação falhou no operador
$geoWithin
, e o motivo é "nenhuma das geometrias consideradas estava contida na geometria da expressão."1 { 2 ... 3 "details": { 4 "operatorName": "$geoWithin", 5 "specifiedAs": { 6 "geoLocation": { 7 "$geoWithin": {...} 8 } 9 }, 10 "reason": "none of the considered geometries were contained within the 11 expression's geometry", 12 "consideredValues": [ -73.9826509, 80.737499 ] 13 } 14 ... 15 }
A validação do esquema é uma ótima ferramenta para impor a governança sobre seus conjuntos de dados. Você tem a opção de Express as regras de validação utilizando JSON schema, MongoDB Query Language ou ambos. E agora, com as mensagens de erro detalhadas, fica ainda mais fácil de usar, e você pode ter as regras tão sofisticadas quanto você precisar, sem o risco de manutenção dispendiosa.
Se você quiser avaliar esse recurso e nos fornecer um feedback antecipado, preencha este formulário para participar do programa de pré-visualização.
Mais postagens sobre validação de esquema:
Questões? Comentários? Queremos muito nos conectar com você. Participe da conversa nos fóruns da MongoDB Community.
Porto seguro
O desenvolvimento, o lançamento e o timing de quaisquer recursos ou funcionalidades descritos dos nossos produtos permanecem a nosso exclusivo critério. Esta informação destina-se apenas a delinear a direção geral do nosso produto e não deve ser invocada na tomada de uma decisão de compra nem é um compromisso, promessa ou obrigação legal de entregar qualquer material, código ou funcionalidade.