So you can replace a property with a number, string, array, or object in JSON.stringify, like so:
var myObj = {
'allstar': aFunction;
}
function myReplacer(key, value) {
if(key === 'allstar') {
return 'newFunction()';
}
}
JSON.stringify(myObj, myReplacer); //returns '{"allstar": "newFunction()"}'
But can you change it so that it instead returns '{"allstar": newFunction()}' (without the quotes around newFunction)?
I assume typeof aFunction == "function"? If so, even JSON.stringify(myObj) will not do what you want it to do, but return '{}' i.e. an object without properties, because functions are not supported in JSON.
Your desired result is not even valid JSON. newFunction() without quotes is not a supported value (string, number, array, object, boolean, null).
Edit: you could try to return newfunction.toString() in your replacer, which should deliver your function's source as string. When converting the JSON back, you then must eval() this string to get the actual function.
#derpirscher provided a very good answer that will probably get more upvotes than this one, but this is my preferred answer:
Based on derpirscher's answer I decided it would be easier to make my own version of JSON.stringify that allows you to replace properties with your own source code, and changed the name of the module so that there is no naming conflict with JSON.
It's on my github account:
https://github.com/johnlarson/xerxes
Related
I have a project using TypeScript and ESLint. I need to deserialize a JSON string, and want to take advantage of the optional reviver parameter. A reviver function basically lets you conditionally transform values as part of the JSON deserialization.
The signature of the reviver function is defined as part of the JSON.parse specification, which is:
JSON.parse(text: string, reviver?: ((this: any, key: string, value: any) => any) | undefined): any
In particular: it takes in a value of type any and returns a value of type any.
const deserializedValue: unknown = JSON.parse(serializedValue, (key, value) => {
if (value === 'foo') {
return 'bar
}
return value
}
I'm scolded by ESLint because when I write return value I am returning something of any type:
5:4 error Unsafe return of an `any` typed value #typescript-eslint/no-unsafe-return
Is there a way for me to programmatically avoid the linting complaint about any types within the constraints of the unknown nature of deserialization, or do I have to disable the linting rule for that line?
eslint is a bit overzealous here. Or at least that rule doesn't apply well to this case.
Parsing JSON is an inherently type unsafe process. In this case the any is just passed through from the argument type, and the function is typed in a place you can't control to return any.
So I'd probably just cast it to unknown like:
return value as unknown
Which sort of makes it clear that "I don't know or care what this is". And the return type does matter because anything matches any and the return type is used in the return type of JSON.parse().
This seems to work.
But that's probably not that much better than disabling the rule for that line either. Which is right is more a matter of opinion.
But still, I'd go with the as unknown cast.
I came across this function and I've been searching for the programming language that has this syntax:
function getDiscount(items: Item[]): number {
if(items.length === 0){
throw new Error("Items cannot be empty");
}
let discount = 0;
items.forEach(function(item) {
discount += item.discount;
})
return discount/100;}
The parameter is delimited by a colon (:), and then the parameter is followed by another colon. I tried to run the code on the console but I'm getting an error "Uncaught SyntaxError: Unexpected token ':'"
The closest that I could find is Python's function annotation, however, the parameter is followed by an arrow instead of a colon.
I would also like to know what the code on the first line means - the parameter and what follows the parameter. My understanding is that the argument that will be passed will be inserted into an array and the data type of what will be returned is a number. Please correct me if I'm wrong.
This code is written in TypeScript (https://www.typescriptlang.org), which is a superset of JavaScript.
TypeScript code is valid JavaScript with added types.
Actually, if you removed the type annotations you could run this code in browser console:
function getDiscount(items) {
if(items.length === 0){
throw new Error("Items cannot be empty");
}
let discount = 0;
items.forEach(function(item) {
discount += item.discount;
})
return discount/100;
}
TypeScript needs to be converted into JavaScript so it can be used in a browser or NodeJS. The conversion process is known as 'transpiling', which is similar to compiling but is more like transformation from one human readable language to another, rather than the typical conversion from a human readable to machine readable. (I updated this description as suggested by Caius Jard)
Type annotations in the definition of the function mean that it takes an array of items of type Item as an argument and returns type number.
From the code we can say that the type Item is an object which has at least one key, discount, of type number. This code will iterate over the array of Item's and return the sum of all discounts.
Consider the following interface within TypeScript
interface IApiCall<TResponse> {
method: string;
url: string;
}
Which is then used within the following method;
const call = <TResponse>(api: IApiCall<TResponse>): void => {
// call to API via ajax call
// on response, grab data
// use JSON.parse(data) to convert to json object
return json as TResponse;
};
Now we use this for Type safety within our methods so we know what objects are being returned from the API. However, when we are returning a single string from the API, JSON.parse is converting the string '12345' into a number, which then breaks further down the line when we are trying to treat this as a string and use value.trim() yet it has been translated into a number.
So ideas to solve this so that we are not converting a string into a number.
How can we stop JSON.parse from converting a single string value into a number?
If using JSON.parse, we check the type of TResponse and compare it against the typeof of json generated.
if (typeof (json) !== typeof(TResponse))...
However there doesn't seem to be an obvious way to determine the generic type.
Question 1: How can we stop JSON.parse() from converting a single string value into a number?
JSON is a text format, so in JSON.parse(x), x needs to be a string. But JSON text represents data of not-necessarily-string types. It sounds like you might be making a category mistake, by confusing a thing with its representation.
If you convert the number 12345 to JSON (JSON.stringify(12345)) you will get the string "12345". If you parse that string, (JSON.parse("12345")), you will get the number 12345 back. If you wanted to get the string "12345", you need to encode it as JSON ( JSON.stringify("12345")) as the string "\"12345\"". If you parse that ( JSON.parse('"12345"') you will get the string "12345" out.
So the straightforward answer to the question "How can we stop JSON.parse() from converting a single string value into a number" is "by properly quoting it". But maybe the real problem is that you are using JSON.parse() on something that isn't really JSON at all. If you are given the string "12345" and want to treat it as the string "12345", then you don't want to do anything at all to it... just use it as-is without calling JSON.parse().
Hope that helps. If for some reason either of those don't work for you, you should post more details about your use case as a Minimal, Complete, and Verifiable example.
Question 2: How do we determine that the returned JSON-parsed object matches the generic type?
In TypeScript, the type system exists only at design time and is erased in the emitted JavaScript code that runs later. So you can't access interfaces and type parameters like TResponse at runtime. The general solution to this is to start with the runtime solution (how would you do this in pure JavaScript) and help the compiler infer proper types at design time.
Furthermore, the interface type IApiCall
interface IApiCall<TResponse> {
method: string;
url: string;
}
has no structural dependence on TResponse, which is not recommended. So even if we write good runtime code and try to infer types from it, the compiler will never be able to figure out what TResponse is.
In this case I'd recommend that you make the IApiCall interface include a member which is a type guard function, and then you will have to write your own runtime test for each type you care about. Like this:
interface IApiCall<TResponse> {
method: string;
url: string;
validate: (x: any) => x is TResponse;
}
And here's an example of how to create such a thing for a particular TResponse type:
interface Person {
name: string,
age: number;
}
const personApiCall: IApiCall<Person> = {
method: "GET",
url: "https://example.com/personGrabber",
validate(x): x is Person {
return (typeof x === "object") &&
("name" in x) && (typeof x.name === "string") &&
("age" in x) && (typeof x.age === "number");
}
}
You can see that personApiCall.validate(x) should be a good runtime check for whether or not x matches the Person interface. And then, your call() function can be implemented something like this:
const call = <TResponse>(api: IApiCall<TResponse>): Promise<TResponse | undefined> => {
return fetch(api.url, { method: api.method }).
then(r => r.json()).
then(data => api.validate(data) ? data : undefined);
};
Note that call returns a Promise<Person | undefined> (api calls are probably asynchronous, right? and the undefined is to return something if the validation fails... you can throw an exception instead if you want). Now you can call(personApiCall) and the compiler automatically will understand that the asynchronous result is a Person | undefined:
async function doPersonStuff() {
const person = await call(personApiCall); // no <Person> needed here
if (person) {
// person is known to be of type Person here
console.log(person.name);
console.log(person.age);
} else {
// person is known to be of type undefined here
console.log("File a missing Person report!")
}
}
Okay, I hope those answers give you some direction. Good luck!
Type annotations only exist in TS (TResponse will be nowhere within the output JS), you cannot use them as values. You have to use the type of the actual value, here it should be enough to single out the string, e.g.
if (typeof json == 'string')
I have a mpgw where the request is JSON.
I save the content in a context variable with JSON.stringify(json)
The problem is when json contains a emoiji eg \uD83D\uDE0D tha variable no longer will be a string, it will be binary and the emoijis is shown as dots.
I need to use the the content of the variable later to calculate hmac so it has to look exact as the original json.
Is there any way to get around this?
Help wold be much appreciated.
We are running firmware: IDG.7.5.2.9
/Jocke D
Ok, from your comment I can conclude that it is the Stringify() that messes it up. This is according to the cookbook for escaping (there is a RFC describing this)...
Try adding your own function for stringify() that will handle unicode better:
function JSON_stringify(s, emit_unicode) {
var json = JSON.stringify(s);
return emit_unicode ? json : json.replace(/[\u007f-\uffff]/g,
function(c) {
return '\\u'+('0000'+c.charCodeAt(0).toString(16)).slice(-4);
}
);
}
ctx.setVar('json', JSON_stringify(json, false));
Something like that...
I'm trying to understand what is going on when I query data from a database and provide it to JSON and then push it to a GUI, in regards to what and when is converting to a string.
If I make a call to a database and provide this data as JSON to the front end, my JSON may look like
MyData{ [["Name": "Anna", "Age": 50],["Name": "Bob", "Age": 40 ]};
Visually it appears as if the value for Name is a string (as it's in quote marks) and the value for Age is an integer (as of no quote marks).
My 2 questions are
Is my understanding that the value for Name is a string and the value of Age is an integer or does JavaScript convert/cast behind the scenes?
How would I specify the type DateTime. According to The "right" JSON date format I actually just ensure the format is correct but ultimately it's a string.
Is my understanding that the value for Name is a string and the value of Age is an integer...?
Almost. It's a number, which may or may not be an integer. For instance, 10.5 is a number, but not an integer.
...or does JavaScript convert/cast behind the scenes?
JavaScript and JSON are different things. JSON defines that the number will be represented by a specific format of digits, . and possibly the e or E character. Whatever environment you're parsing the JSON in will map that number to an appropriate data type in its environment. In JavaScript's case, it's an IEEE-754 double-precision binary floating point number. So in that sense, JavaScript may cast/convert the number, if the JSON defines a number that can't be accurately represented in IEEE-754, such as 9007199254740999 (which becomes the number 9007199254741000 because IEEE-754 only has ~15 digits of decimal precision).
How would I specify the type DateTime. According to The "right" JSON date format I actually just ensure the format is correct but ultimately it's a string.
JSON has a fixed number of types: object, array, string, number, boolean, null. You can't add your own. So what you do is encode any type meta-data you want into either the key or value, or use an object with separate properties for type and value, and enforce that agreement at both ends.
An example of doing this is the common format for encoding date/time values: "/Date(1465198261547)/". As far as the JSON parser is concerned, that's just a string, but many projects use it as an indicator that it's actually a date, with the number in the parens being the number of milliseconds since The Epoch (Jan 1 1970 at midnight, GMT).
Two concepts related to this are a replacer and a reviver: When converting a native structure to JSON, the JSON serializer you're using may support a replacer which lets you replace a value during the serialization with another value. The JSON parser you're using may support a reviver that lets you handle the process in the other direction.
For example, if you were writing JavaScript code and wanted to preserve Dates across a JSON layer, you might do this when serializing to JSON:
var obj = {
foo: "bar",
answer: 42,
date: new Date()
};
var json = JSON.stringify(obj, function(key, value) {
var rawValue = this[key];
if (rawValue instanceof Date) {
// It's a date, convert it to our special format
return "/Date(" + rawValue.getTime() + ")/";
}
return value;
});
console.log(json);
That uses the "replacer" parameter of JSON.stringify to encode dates in the way I described earlier.
Then you might do this when parsing that JSON:
var json = '{"foo":"bar","answer":42,"date":"/Date(1465199095286)/"}';
var rexIsDate = /^\/Date\((-?\d+)\)\/$/;
var obj = JSON.parse(json, function(key, value) {
var match;
if (typeof value == "string" && (match = rexIsDate.exec(value)) != null) {
// It's a date, create a Date instance using the number
return new Date(+match[1]);
}
return value;
});
console.log(obj);
That uses a "reviver" to detect keys in the special format and convert them back into Date instances.
The specific format of a JSON string can be described by JSON Schema, as described here; in particular, date-time apparently is a type already defined in JSON Schema.