I have the following json schema.
const myJson = {
"type": "typeName"
"firstName": "Steven",
"lastName": "Smith",
"address": {
"primary": {
"city": "abc",
"street": {
"name": {
"subName": "someName"
}
}
}
}
}
And I want to loop over each of the properties for required validation on this json, I have the following code so far which works if the property in the json is not nested.
let errors = [];
const typeName = ['firstName', 'lastName'],
const typeAttr = Object.keys(myJson);
typeName.forEach(attr => {
if (!typeAttr.includes(attr)) {
errors.push(`Missing field: ${attr}`);
}
});
How can I add the nested json property like primary, city, street and validate the way I have done it.
Here's how you can check nested properties on json object
const myJson = {
type: "typeName",
firstName: "Steven",
lastName: "Smith",
address: {
primary: {
city: "abc",
street: {
name: {
subName: "someName",
},
},
},
},
};
let errors = [];
const typeName = ["firstName", "lastName", "address.primary", "address.primary.city"];
function checkNested(obj, level, ...rest) {
if (obj === undefined) return false;
if (rest.length == 0 && obj.hasOwnProperty(level)) return true;
return checkNested(obj[level], ...rest);
}
typeName.forEach((attr) => {
let props = attr.split(".");
if (!checkNested(myJson, ...props)) {
errors.push(`Missing field: ${attr}`);
}
});
console.log(errors);
For reference on how to check Nested properties you can check this answer.
Test for existence of nested JavaScript object key
I would do something like this. This method gives whether the data is having all the provided keys or not i.e., will return either true or false
let obj = {"type":"typeName","firstName":"Steven","lastName":"Smith","address":{"primary":{"city":"abc","street":{"name":{"subName":"someName"}}}}};
const typeName = ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.city', 'address.primary.street'];
const validate = (data, types) => {
return types.every(type => {
// Split the keys using `.`
const keys = type.split('.');
// Check if the length is more than 1,
// if yes, then we need to check deeply
if (keys.length > 1) {
let datum = {
...data
};
// Check through all the keys found using split
for (let key of keys) {
// if any key is not found or falsy then return false
if (!datum[key]) {
return false;
}
datum = datum[key];
}
return true;
} else {
// if the length of the keys is not more than 1 then it means
// the key is at the top level and return the value as boolean
return !!data[type]
}
})
}
console.log(validate(obj, typeName));
console.log(validate(obj, ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.zip']));
This below method will return the keys that were not present in the provided data
const validate = (data, types) => {
let errors = [];
types.forEach(type => {
const keys = type.split('.');
let datum = {
...data
};
// Loop through the keys
for (let [index, key] of keys.entries()) {
// Check if the key is not available in the data
// then push the corresponding key to the errors array
// and break the loop
if (!datum[key]) {
errors.push(keys.slice(0, index + 1).join('.'));
break;
}
datum = datum[key];
}
})
return errors;
}
const obj = {"type":"typeName","firstName":"Steven","lastName":"Smith","address":{"primary":{"city":"abc","street":{"name":{"subName":"someName"}}}}};
const typeName = ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.city', 'address.primary.street'];
console.log(validate(obj, ['firstName', 'lastName']));
console.log(validate(obj, ['firstName', 'lastName', 'address', 'address.primary', 'address.primary.zip']));
console.log(validate(obj, [...typeName, 'test', 'address.primary.zip', 'address.test.zip']));
Use JSON Schema, a proposed IETF standard with tons of library implementations available for several languages for describing, generating, and validating JSON documents.
To require your JSON to have all the properties be present, you'll need a JSON Schema like below.
let typeNameSchema = {
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://example.com/typename.schema.json",
"title": "Type Name",
"description": "A person's name and address details",
"type": "object",
"required": [
"firstName",
"lastName",
"address"
],
"properties": {
"type": {
"type": "string"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"address": {
"type": "object",
"required": [
"primary"
],
"properties": {
"primary": {
"type": "object",
"required": [
"city",
"street"
],
"properties": {
"city": {
"type": "string"
},
"street": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "object",
"required": [
"subName"
],
"properties": {
"subName": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
Then use any library to validate your JSON data against the above schema. jsonschema is one of the most popular JSON Schema validators for JavaScript. Here's how to validate your myJson data against the above typeNameSchema.
const Validator = require('jsonschema').Validator;
const validator = new Validator();
console.log(validator.validate(myJson, typeNameSchema)); // result
Validation results will be returned in a ValidatorResult object with the most important properties valid of type boolean and errors of type ValidationError[]. ValidationError object will have the property name and error message.
Related
I am trying to move everything in the Array Results outside and into the original object
this is the object
{
"Name": "John",
"Results": [
{
"Type": "DB",
"Immediate_Action": "No",
}
]
}
It should look like this
{
"Name": "John",
"Type": "DB",
"Immediate_Action": "No",
}
What I have so far is this
const mapOscarResults = ({ data }) => {
return data.map(entry => {
let mapped = {...entry};
entry.Results.forEach(key => {
let Type = mapped[key.Type]
if (mapped[key]) {
mapped[key].push(entry.Results[key]);
} else {
mapped[key] = [entry.Results[key]];
}
});
return mapped;
});
};
You can simply spread the Results array into an Object.assign() call.
const input = { "Name": "John", "Results": [{ "Type": "DB", "Immediate_Action": "No", }, { "Another": "value" }] };
const { Results, ...refactored } = input;
Object.assign(refactored, ...Results);
console.log(refactored)
This code works for your example:
const { Results: results, ...rest } = {
"Name": "John",
"Results": [
{
"Type": "DB",
"Immediate_Action": "No",
}
]
}
const res = {...rest, ...results.reduce((prev, curr) => ({
...prev,
...curr
}), {})}
console.log(res)
But I don't know what you expect when the Results array has more than one element.
In that condition, if this code does not fill your needs, ask me to change it.
however, it will join first Result with index 0, you can expand it
const data = {
"Name": "John",
"Results": [
{
"Type": "DB",
"Immediate_Action": "No",
}
]
}
const mapOscarResults = (data) => {
for (let i in Object.keys(data)){
if (Array.isArray(data[Object.keys(data)[i]])){
newKey = data[Object.keys(data)[i]][0]
data = {... data, ...newKey}
delete data[Object.keys(data)[i]]
}
}
return data
};
console.log(mapOscarResults(data))
there is a list of users
filterData = [
{
"position":"lawyer",
"department_positions":[],
"group_positions":[
{"group":{"id":2,"code":"234","name":"group1"},"lead":false},
{"group":{"id":1,"code":"123","name":"group12"},"lead":true}
]
},
{
"position":"director",
"department_positions":[
{"department":{"id":3,"code":"333","name":"subDep"},"lead":false}
],
"group_positions":[
{"group":{"id":2,"code":"234","name":"group1"},"lead":false},
{"group":{"id":1,"code":"123","name":"group12"},"lead":true}
]
},
{
"position":"director",
"department_positions":[],
"group_positions":[]
}
]
and list of filters
categories = {
"position":["lawyer","director"],
"group_positions":["group1","group12"],
"department_positions":["generalDep", "subDep"]
}
It is necessary to filter users taking into account the fact that several filters can be selected at the same time. For example, i want to find user with position = "director" and AND group_positions = "group1" AND department_positions = "subDep"
my code doesn't allow filtering by multiple conditions. how can i fix it?
this.filter = this.filterData.filter(item => {
for (let key in this.categories) {
if (item[key].find(el =>
this.categories[key].includes(
el.group?.name || el.department?.name
)
)) {
return true
}
}
return false
})}
This is a good place to employ an es6 class to give behavior to the object being filtered. Augment each object to determine if it matches the "category" object.
(from the example data, this assumes the OP is looking for a "product of sums" match: for all of the category keys match at least one of the category values)
class FilterMe {
constructor(item) {
Object.assign(this, item);
}
namesForKey(key) {
switch (key) {
case 'position':
return [this.position]; // always answer an array
case 'group_positions':
return this.group_positions.map(gp => gp.group.name);
case 'department_positions':
return this.department_positions.map(dp => dp.department.name);
default:
return [];
}
}
// return true if a single filter key-value pair is matched
matchesFilterKeyValue(filterKey, filterOptions) {
const myNames = this.namesForKey(filterKey);
const matches = filterOptions.filter(e => myNames.includes(e));
return matches.length > 0;
}
// return true if all filter key-values pairs are matched
matchesFilter(filter) {
return Object.entries(filter).every(keyValue => {
return this.matchesFilterKeyValue(...keyValue);
})
}
}
const filterData = [{
"position": "lawyer",
"department_positions": [],
"group_positions": [{
"group": {
"id": 2,
"code": "234",
"name": "group1"
},
"lead": false
}, {
"group": {
"id": 1,
"code": "123",
"name": "group12"
},
"lead": true
}]
},
{
"position": "director",
"department_positions": [{
"department": {
"id": 3,
"code": "333",
"name": "subDep"
},
"lead": false
}],
"group_positions": [{
"group": {
"id": 2,
"code": "234",
"name": "group1"
},
"lead": false
}, {
"group": {
"id": 1,
"code": "123",
"name": "group12"
},
"lead": true
}]
},
{
"position": "director",
"department_positions": [],
"group_positions": []
}
]
const categories = {
"position": ["lawyer", "director"],
"group_positions": ["group1", "group12"],
"department_positions": ["generalDep", "subDep"]
}
// convert the filterData to the objects and test them...
let objects = filterData.map(d => new FilterMe(d));
let matches = objects.filter(o => o.matchesFilter(categories))
console.log(matches)
You can try something like this:
let filtered = example.filter(item => {
let valid = false
if (item.includes('something')) {
valid = true
}
if (!valid) {
// check second condition
}
return valid
})
Use a temporary placeholder so you don't immediately have to return true/false.
I get an input like this:
input 1:
{
"name": "Ben",
"description": "Ben",
"attributes": [
{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
input 2
{
"name": "Ice",
"description": "Ice",
"attributes": [
{
"type": "Background",
"value": "Green"
},
{
"type": "Hair-color",
"value": "White"
}
]
}
input 3
{
"name": "Itay",
"description": "Itay",
"attributes": [
{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
What I want to do is count the amount of each type of background and each type of hair-color appearing.
(These are sample examples and in reality there are more types and different values)
Let's say in these examples we have 2 objects that have a background as default then I want to have a count of that like so:
export interface TraitCount {
value: string,
count: number
}
export interface CountOfEachAttribute {
trait_type: string,
trait_count: traitCount[] | null,
total_variations: number
}
I want the most effective code because there are other aspects to the code, in addition it will run on 5-10k queries not just three, so needs
to run in good times too :D
(It's similar to my other question done with python but now I need it in js also)
Atm it's something like this:
(Apart of a much bigger code so keep that in mind)
setInitalCountOfAllAttribute( state, { payload }: PayloadAction<CountOfEachAttribute[] | null> ) {
if (payload === null) {
state.countOfAllAttribute = null;
} else {
state.countOfAllAttribute = payload;
}
},
setCountOfAllAttribute(state, { payload }: PayloadAction<Attribute>) {
if (state.countOfAllAttribute !== null) {
state.countOfAllAttribute.map(
(countOfEachAttribute: CountOfEachAttribute) => {
// Find the trait type
if (countOfEachAttribute.trait_type === payload.trait_type) {
// initiate the trait count array to store all the trait values and add first trait value
if (countOfEachAttribute.trait_count === null) {
const new_trait_count = { value: payload.value, count: 1 };
countOfEachAttribute.trait_count = [new_trait_count];
countOfEachAttribute.total_variations++;
}
// Trait array already existed.
else {
// Check if value already present or not
const checkValue = (obj: any) => obj.value === String(payload.value);
const isPresent = countOfEachAttribute.trait_count.some(checkValue)
const isPresent2 = countOfEachAttribute.trait_count.find((elem: any) => elem.value === String(payload.value))
// Value matched, increase its count by one
if (isPresent2) {
countOfEachAttribute.trait_count &&
countOfEachAttribute.trait_count.map((trait) => {
if (trait.value === payload.value) {
trait.count++;
}
});
}
// Value doesn't match, add a new entry and increase the count of variations by one
else {
const new_trait_count = { value: payload.value, count: 1 };
countOfEachAttribute.trait_count = [
...countOfEachAttribute.trait_count,
new_trait_count,
];
countOfEachAttribute.total_variations++;
}
}
}
}
);
}
},
You can merge all arrays and use Array.reduce.
const input1 = {
"name": "Ben",
"description": "Ben",
"attributes": [{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
const input2 = {
"name": "Ice",
"description": "Ice",
"attributes": [{
"type": "Background",
"value": "Green"
},
{
"type": "Hair-color",
"value": "White"
}
]
}
const input3 = {
"name": "Itay",
"description": "Itay",
"attributes": [{
"type": "Background",
"value": "Default"
},
{
"type": "Hair-color",
"value": "Brown"
}
]
}
const mergedInput = [input1, input2, input3];
const result = mergedInput.reduce((acc, item) => {
item.attributes.forEach(attrItem => {
const existType = acc.find(e => e.trait_type == attrItem.type);
if (existType) {
var existAttr = existType.trait_count.find(e => e.value == attrItem.value);
if (existAttr) {
existAttr.count++;
} else {
existType.trait_count.push({
value: attrItem.value,
count: 1
});
existType.total_variations++;
}
} else {
acc.push({
trait_type: attrItem.type,
trait_count: [{
value: attrItem.value,
count: 1
}],
total_variations: 1
})
}
});
return acc;
}, []);
console.log(result);
I suggest instead of creating an array for trait_count to make it an object so you don't have to iterate over it whenever you are adding a new attribute. In the snippet below I'm using the value of the attribute as a sort of hash that allows the access to the given property without having to call the Array.prototype.find function
const input1 = {"name":"Ben","description":"Ben","attributes":[{"type":"Background","value":"Default"},{"type":"Hair-color","value":"Brown"}]};
const input2 = {"name":"Ice","description":"Ice","attributes":[{"type":"Background","value":"Green"},{"type":"Hair-color","value":"White"}]};
const input3 = {"name":"Itay","description":"Itay","attributes":[{"type":"Background","value":"Default"},{"type":"Hair-color","value":"Brown"}]};
function countAtributes(input, totalCounts={}) {
input.attributes.forEach((attribute) => {
if (!totalCounts[attribute.type])
totalCounts[attribute.type] = {trait_type: attribute.type, trait_count: {}, total_variations: 0};
if (!totalCounts[attribute.type].trait_count[attribute.value]) {
totalCounts[attribute.type].trait_count[attribute.value] = {value: attribute.value, count: 1};
totalCounts[attribute.type].total_variations+=1;
}
else totalCounts[attribute.type].trait_count[attribute.value].count +=1;
})
}
const totalCounts = {};
countAtributes(input1, totalCounts);
countAtributes(input2, totalCounts);
countAtributes(input3, totalCounts);
console.log(totalCounts);
It could be turned into the array afterwards with Object.values if necessary
I believe it is a much better approach to what you had before as you don't have to iterate over the tables of trait_counts. In theory it should significantly reduce the time taken. Iterating over the array and checking a condition each time is much slower than key lookup in Javascript object
I want to develop this functionality for searching/filtering a list. Basically, I'll get a search term from an input box and then I have to get all the items that include the search term from an array.
Here's what I've tried so far, it works for root level properties but doesn't work with nested arrays/objects:
// Filter List
this.filterList = query => {
if (typeof query === "string") {
// transform query to lowercase
query = query.toLowerCase();
// clear the current list being displayed
this.filteredList = [];
// filter the lsit and store the results with
// matching criteria in "filteredList" array
let filteredResults = _.filter(this.itemList, item => {
if (item && typeof item === "object") {
// loop over the item object
for (let property in item) {
if (item.hasOwnProperty(property)) {
let key = item[property];
// address, phone and emails
if (typeof key === "object" && _.isArray(key)) {
_.filter(key, element => {
if (typeof element === "object") {
for (let nestedProperty in element) {
let nestedKey = element[nestedProperty];
if (nestedKey) {
nestedKey = nestedKey.toString().toLowerCase();
}
if (nestedKey && nestedKey.includes(query)) {
return item;
}
}
}
});
} else {
if (key) key = key.toString().toLowerCase();
if (key && key.includes(query)) return item;
}
}
}
}
});
// assign the filtered results to the list being displayed
this.filteredList = [...filteredResults];
} else {
// if query is empty or null or anything other than string
// revert all changes and assign the original list to display list
this.filteredList = this.itemList;
}
};
If it helps, here's an object from the array that I am looping over:
[
{
"id": "number",
"dealerCode": "string",
"name": "string",
"gstin": "string",
"pan": "string",
"cin": "string",
"emails": [
{
"name": "string",
"address": "string",
"isPrimary": "boolean"
}
],
"phoneNumbers": [
{
"countryCode": "number",
"number": "number",
"isPrimary": "boolean"
}
],
"addresses": [
{
"addressLine1": "string",
"addressLine2": "string",
"addressLine3": "string",
"country": "string",
"state": "string",
"city": "string",
"postalCode": "number",
"isPrimary": "boolean"
}
],
"status": "string",
"statusId": "number"
}
]
I am doing this in AngularJS and using Lodash as well.
For a problem like this where you need to compare a heterogenous list of primitives and object/arrays, a recursive named function is usually the best way to go. This should probably solve what you're looking for, based on the following assumptions:
All entries by a user as treated as strings. So 99 and "99" are the same. I'll comment in the code where this assumption is made
All entries are case insensitive (all converted toLowercase)
There is no set depth of the nested objects/arrays; the solution below works recursively for any depth of a heterogeneous list
If anything matches in any leaf node, the entire object will be returned
The way the solution works below is:
Filter through the top level list and call matchesEntryInTree on each dataItem, compared to the userEntry
matchesEntryInTree will check each dataItem and see if it's an array or object
If the dataItem is an array/object, we drill into them again by calling matchesEntryInTree recursively
If it isn't, we call compareValues to see if the entry matches the current dataItem
With the recursive pattern above, all leaf nodes (regardless of the shape of the tree) will be compared to the initial userEntry
// test data for trial runs
const testData = [
{
id: 123488,
dealerCode: "ACb3",
name: "Some Name",
gstin: "string",
pan: "string",
cin: "string",
emails: [
{
name: "Some Email name",
address: "anemail.domain.com",
isPrimary: "boolean"
}
],
phoneNumbers: [
{
countryCode: "9398",
number: "number",
isPrimary: "boolean"
}
],
addresses: [
{
addressLine1: "Florida",
addressLine2: "Street place",
addressLine3: "string",
country: "string",
state: "string",
city: "string",
postalCode: "number",
isPrimary: "boolean"
}
],
status: "string",
statusId: "number"
},
{
id: 88888,
dealerCode: "NMC",
name: "Some Other",
gstin: "string",
pan: "string",
cin: "string",
emails: [
{
name: "An Email thing",
address: "athing.somewhere.org",
isPrimary: "boolean"
}
],
phoneNumbers: [
{
countryCode: "93948",
number: "number",
isPrimary: "boolean"
}
],
addresses: [
{
addressLine1: "Denver",
addressLine2: "Street place",
addressLine3: "string",
country: "string",
state: "string",
city: "string",
postalCode: "number",
isPrimary: "boolean"
}
],
status: "string",
statusId: "number"
}
];
// broke these into separate helper functions, but you can combine all of them except the recursive one if you'd like
const returnFilterResults = (userEntry, dataItems) => {
const compareValues = (entry, dataValue) => {
if ( _.isBoolean(dataValue)) {
return entry === dataValue;
} else if (_.isNumber(dataValue)) {
// if the dataValue is a number, we convert both it and the user's entry (which probably already is a string) to a string to compare
// you can make this comparison more strict if desired
return _.includes(_.toLower(_.toString(dataValue)), _.toLower(entry));
} else if (_.isString(dataValue)) {
return _.includes(_.toLower(dataValue), _.toLower(entry));
} else {
return false;
}
};
const matchesEntryInTree = (entry, dataItem) => {
// if this dataItem is an object or array, let's drill back in again
if (_.isObject(dataItem) || _.isArray(dataItem)) {
// as we recursively move through the tree, check to see if *any* of the entries match, using 'some'
return _.some(dataItem, innerDataItem => {
return matchesEntryInTree(entry, innerDataItem);
});
} else {
// if it's a primitive, then let's compare directly
return compareValues(entry, dataItem);
}
};
// I created a results variable so we could console log here in this snippet
// but you can just return from the filter directly
const results = _.filter(dataItems, dataItem => {
return matchesEntryInTree(userEntry, dataItem);
});
console.log(userEntry, results);
return results;
};
returnFilterResults("place", testData);
// both entries return
returnFilterResults("Denver", testData);
// second entry is returned
returnFilterResults(48, testData);
// both entries return - ID in first, countryCode in second
returnFilterResults(12, testData);
// first entry is returned
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
Why don't you use a flatten function to flatten your object/JSON then search for your value? An example for this is the following:
var flattenObject = function(ob) {
var toReturn = {};
for (var i in ob) {
if (!ob.hasOwnProperty(i)) continue;
if ((typeof ob[i]) == 'object') {
var flatObject = flattenObject(ob[i]);
for (var x in flatObject) {
if (!flatObject.hasOwnProperty(x)) continue;
toReturn[i + '.' + x] = flatObject[x];
}
} else {
toReturn[i] = ob[i];
}
}
return toReturn;
};
For a nested object, let's say
{
A : {
B: {
C: "V"
}
}
}
you'll get an object with the key A.B.C and the value "V". This way, you'll only have one level to search your value in.
Hope this helps!
Cheers!
I use ajv to validate JSON data model before inserting/updating my database.
Today I want to use this structure:
const dataStructure = {
xxx1234: { mobile: "ios" },
yyy89B: { mobile: "android" }
};
My keys are dynamic because they are ids.
Do you know how to validate it with ajv ?
PS: as an alternative solution, i can of course use this structure:
const dataStructure = {
mobiles: [{
id: xxx1234,
mobile: "ios"
}, {
id: yyy89B,
mobile: "android"
}]
};
Then I would have to loop on the array to find the ids I want.
All my queries will become more complex, it bothers me.
Thank you for your help !
Below example may help you.
1.Validate dynamic key value
Update regex with your requirement.
const dataStructure = {
11: { mobile: "android" },
86: { mobile: "android" }
};
var schema2 = {
"type": "object",
"patternProperties": {
"^[0-9]{2,6}$": { //allow key only `integer` and length between 2 to 6
"type": "object"
}
},
"additionalProperties": false
};
var validate = ajv.compile(schema2);
console.log(validate(dataStructure)); // true
console.log(dataStructure);
2.Validate array of JSON with simple data types.
var schema = {
"properties": {
"mobiles": {
"type": "array", "items": {
"type": "object", "properties": {
"id": { "type": "string" },
"mobile": { "type": "string" }
}
}
}
}
};
const data = {
mobiles: [{
id: 'xxx1234',
mobile: "ios"
}]
};
var validate = ajv.compile(schema);
console.log(validate(data)); // true
console.log(data);
You can add your validation as per requirement.