Let's say I want to get a data from Visual Studio TFS and the response (as json) is in this kind of format:
{
"Microsoft.VSTS.Scheduling.StoryPoints": 3.0,
// ......
}
There's dot in the property name. Reading from other questions I found out that I can read that json in typescript by using an interface like this
export interface IStory { // I don't think this kind of interface do me any help
"Microsoft.VSTS.Scheduling.StoryPoints": number
}
And then I can use the property with this syntax:
var story = GetStoryFromTFS();
console.log(story["Microsoft.VSTS.Scheduling.StoryPoints"]);
But I'd prefer not to call the property like this, since the intellisense won't able to help me finding which property I want to use (because I call the property using a string).
In C# there is a JsonProperty attribute which enable me to create a model like this:
public class Story
{
[JsonProperty(PropertyName = "Microsoft.VSTS.Scheduling.StoryPoints")]
public double StoryPoints { get; set; }
}
And then I can use the property this way:
var story = GetStoryFromTFS();
Console.WriteLine(story.StoryPoints);
This way the intellisense will able to help me finding which property I want to use.
Is there something like JsonProperty attribute in typescript? Or is there any other, better way, to achieve this in typescript?
You have many options. Just keep in mind that all of these options require you to pass the original data to the class that will access it.
Map the values.
class StoryMap {
constructor(data: IStory) {
this.StoryPoints = data["Microsoft.VSTS.Scheduling.StoryPoints"];
}
StoryPoints: number;
}
Wrap the data.
class StoryWrap {
constructor(private data: IStory) {}
get StoryPoints(): number { return this.data["Microsoft.VSTS.Scheduling.StoryPoints"] };
}
Build a decorator to map the data.
function JsonProperty(name: string) {
return function DoJsonProperty(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.get = function () {
return this.data[name];
}
descriptor.set = function (value) {
this.data[name] = value;
}
}
}
class StoryDecorator
{
constructor(private data: IStory) {}
#JsonProperty("Microsoft.VSTS.Scheduling.StoryPoints")
get StoryPoints(): number { return 0 };
}
Related
I have a problem that I can't seem to find a solution for or maybe my search is wrong. The problem is as follows:
Background
I have an BookingEvent class that is defined as follows:
export class BookingEvent {
title: string;
private _startDate: Date;
set startDate(value: Date) {
let valueMoment = moment(value);
if (valueMoment.isValid()) this._startDate = valueMoment.toDate();
}
get startDate(): Date {
return this._startDate;
}
private _endDate: Date;
set endDate(value: Date) {
let valueMoment = moment(value);
if (valueMoment.isValid()) this._endDate = valueMoment.toDate();
}
}
In addition to the above, I have a form component with a template doing two way binding to the properties.
Problem
When I save the form data to a remote api, I realised that the json produced maps my startDate & endDate bound properties as _startDate & _endDate respectively as is evident from:
{
"title": "My awesome event",
"_startDate": "2018-04-26T20:50:00.000Z",
"_endDate": "2018-04-27T19:50:00.000Z"
}
Question
Why is the native json mapper using the private property names as opposed to the public ones? How can I ensure that the public property name are used?
You can specify a toJSON() method to customize what the object looks like when it is serialized using JSON.stringify(...). That link details exactly how it figures out what to include in the object being serialized. JavaScript doesn't have the concept of private properties, so they are just properties like anything else.
class BookingEvent {
// all the rest of your code
toJSON() {
return {
title: this.title,
startDate: this.startDate,
endDate: this.endDate
}
}
}
I am using getter/setter accessors in TypeScript. As it is not possible to have the same name for a variable and method, I started to prefix the variable with a lower dash, as is done in many examples:
private _major: number;
get major(): number {
return this._major;
}
set major(major: number) {
this._major = major;
}
Now when I use the JSON.stringify() method to convert the object into a JSON string, it will use the variable name as the key: _major.
As I don't want the JSON file to have all keys prefixed with a lower dash, is there any possibility to make TypeScript use the name of the getter method, if available? Or are there any other ways to use the getter/setter methods but still produce a clean JSON output?
I know that there are ways to manually modify the JSON keys before they are written to the string output. I am curious if there is simpler solution though.
Here is a JSFiddle which demonstrates the current behaviour.
No, you can't have JSON.stringify using the getter/setter name instead of the property name.
But you can do something like this:
class Version {
private _major: number;
get major(): number {
return this._major;
}
set major(major: number) {
this._major = major;
}
toJsonString(): string {
let json = JSON.stringify(this);
Object.keys(this).filter(key => key[0] === "_").forEach(key => {
json = json.replace(key, key.substring(1));
});
return json;
}
}
let version = new Version();
version.major = 2;
console.log(version.toJsonString()); // {"major":2}
based on #Jan-Aagaard solution I have tested this one
public toJSON(): string {
let obj = Object.assign(this);
let keys = Object.keys(this.constructor.prototype);
obj.toJSON = undefined;
return JSON.stringify(obj, keys);
}
in order to use the toJSON method
I think iterating through the properties and string manipulating is dangerous. I would do using the prototype of the object itself, something like this:
public static toJSONString() : string {
return JSON.stringify(this, Object.keys(this.constructor.prototype)); // this is version class
}
I've written a small library ts-typed, which generate getter/setter for runtime typing purpose. I've faced the same problem when using JSON.stringify(). So i've solved it by adding a kind of serializer, and proposing to implement a kind of toString (in Java) buy calling it toJSON.
Here is an example:
import { TypedSerializer } from 'ts-typed';
export class RuntimeTypedClass {
private _major: number;
get major(): number {
return this._major;
}
set major(major: number) {
this._major = major;
}
/**
* toString equivalent, allows you to remove the _ prefix from props.
*
*/
toJSON(): RuntimeTypedClass {
return TypedSerializer.serialize(this);
}
}
A new answer to an old question. For situations where there is no private field for a getter/setter, or where the private field name is different to the getter/setter, we can use the Object.getOwnPropertyDescriptors to find the get methods from the prototype.
https://stackoverflow.com/a/60400835/2325676
We add the toJSON function here so that it works with JSON.stringify as mentioned by other posters. This means we can't call JSON.stringify() within toJSON as it will cause an infinite loop so we clone using Object.assign(...)
I also removed the _private fields as a tidyup measure. You may want to remove other fields you don't want to incude in the JSON.
public toJSON(): any {
//Shallow clone
let clone: any = Object.assign({}, this);
//Find the getter method descriptors
//Get methods are on the prototype, not the instance
const descriptors = Object.getOwnPropertyDescriptors(Object.getPrototypeOf(this))
//Check to see if each descriptior is a get method
Object.keys(descriptors).forEach(key => {
if (descriptors[key] && descriptors[key].get) {
//Copy the result of each getter method onto the clone as a field
delete clone[key];
clone[key] = this[key]; //Call the getter
}
});
//Remove any left over private fields starting with '_'
Object.keys(clone).forEach(key => {
if (key.indexOf('_') == 0) {
delete clone[key];
}
});
//toJSON requires that we return an object
return clone;
}
Isn't dynamic but it work
export class MyClass{
text: string
get html() {
return this.text.toString().split("\n").map(e => `<p>${e}</p>`).join('');
}
toJson(): string {
return JSON.stringify({ ...this, html: this.html })
}
}
In calling
console.log(myClassObject.toJson())
I am using Cerialize as Json serializer/deserializer.
My Api connector works this way:
ApiService.get<T>(endpoint:string):Promise<T>
calls
ApiConnectorService.get<ApiResponse<T>>(tClazz:{new():T;}, endpoint:string);
to deserialize, Cerialize uses class parameter:
function Deserialize(json: any, type?: Function | ISerializable): any;
So, when I call ApiService.get<T>(endpoint), I call ApiConnectorService.get<ApiResponse<T>>(ApiResponse, endpoint) in it.
Problem
I can't provide ApiResponse<T> as tClazz parameter, compiler says
TS1109: Expression expected
Is there a way to provide Generic class with its Generic type as parameter? This way, when I call get<User>() I get a User in an ApiResponse<User> type, at the moment I only get an Object in ApiResponse, which is not whet we need.
Here is the ApiResponse class:
export class ApiResponse<T>{
#deserialize
data:T;
#deserializeAs(ErrorData)
error:ErrorData;
#deserialize
meta:Object;
}
EDIT: Same error if I want to give an array as class parameter:
ApiService.get<Foo[]>(Foo[], '/bar');
TS1109: Expression expected
You cannot, if you look at the transpiled generic class here: Typescript Playground it looses the generics, so in runtime you have no types so you can't get the type of the generic, meaning, in runtime you cannot know what type is your T. You have to pass the class itself as a parameter. You can use the generics for compilation help, but thats the best you can do.
Taking into account that nested generics are not supported, and generics are just compile time construct, I have made a small sample that I hope might be helpful to you:
export class User
{
private name: string;
constructor(serializedValue: string)
{
this.name = serializedValue;
}
}
export class ApiResponse<T>
{
constructor(val: T)
{
this.data = val;
}
data:T;
error:string;
meta:Object;
}
class ApiConnectorService<T>
{
public get<U extends ApiResponse<T>>(tClazz: {new(s: string):T;}, uClazz: {new(t: T):U;}, endpoint:string): U
{
let val = 'user';//get serializedvalue from endpoint
return new uClazz(new tClazz(val));
}
}
class ApiService
{
public get<T>(tClazz: {new(s: string):T;}, endpoint:string): T
{
let s = new ApiConnectorService<T>();
return s.get(tClazz, ApiResponse, '123').data;
}
}
let a = new ApiService();
console.log(a.get<User>(User, '12'));
Consider the following example dataclass:
[RemoteClass]
public class SOTestData {
public var i:int;
public function SOTestData(i:int) {
this.i = i;
}
}
As I understand, the RemoteClass metadata-tag should ensure that when an object of this class gets sreialized, the type information is preserved.
I used the following program to test:
public class SOTest extends Sprite {
public function SOTest() {
var data:SharedObject = SharedObject.getLocal("SOTest");
if (data.data.object) {
try {
var stored:SOTestData = data.data.object;
trace(stored.i);
} finally {
data.clear();
}
}
else {
data.data.object = new SOTestData(15);
data.flush();
}
}
}
Here the first run writes the data, seconds reads and clears. Running this, I still get a class cast error. Indeed, in the SharedObject there is no type information stored.
I don't think i'm using the metadata wrong, could it maybe be that the compiler doesn't know what to do with it? I don't get any compiler errors/warnings, although when i use some inexistant tag it doesn't complain either. I'm using Flex 4.6 SDK with FlashDevelop as IDE.
EDIT:
Below is the shared object. As you can see, the type is saved as "Object" instead of the actual type.
so = [object #2, class 'SharedObject'] {
data: [object #0, class 'Object'] {
object: [object #1, class 'Object', dynamic 'False', externalizable 'False'] {
i: 15,
},
}
}
I've only used RemoteClass for making AMF RemoteObject calls; I didn't think it had anything to do w/ Shared Objects. Per the docs
Use the [RemoteClass] metadata tag to register the class with Flex so
that Flex preserves type information when a class instance is
serialized by using Action Message Format (AMF). You insert the
[RemoteClass] metadata tag before an ActionScript class definition.
The [RemoteClass] metadata tag has the following syntax:
As best I can tell from the code you provided, you are not serializing the object in AMF format.
I believe your class cast error is due to the fact that you aren't casting your class. Shared Objects always come back as generic Objects. Try this:
var stored:SOTestData = data.data.object as SOTestData ;
Here is some code from an application I use. First the value object which will get serialized in a shared object:
package com.login.vos
{
[RemoteClass(alias="com.login.vos.UserVO")]
public class UserVO
{
public function UserVO()
{
}
public var firstName :String;
public var lastName :String;
public var userID :Number;
}
}
The the code to save the object:
public static function saveUserVO(userVO:UserVO):void{
var userSharedObject :SharedObject = SharedObject.getLocal('userVO') ;
userSharedObject.data.userVO = userVO;
userSharedObject.flush();
}
And finally, the code to load the objecT:
public static function getUserVO():UserVO{
var userSharedObject :SharedObject = SharedObject.getLocal('userVO')
if(userSharedObject.size <=0){
return null;
}
return userSharedObject.data.userVO as UserVO;
}
The only obvious difference between this and the code by the original poster is that I'm specifying an alias in the RemoteClass metadata.
I'm starting out with GXT and am a bit confused on how to use the JsonReader to parse a valid json string with multiple root objects. I have a set of selection boxes to build the sql statement to display records in a grid. I'm getting the values for the selections from the database as well. What I'm attempting to do is a single request to my php database functions to build the json with all the values for the selection boxes in one string. My first thought was the JsonReader.
Here's an example of the json I'm working with:
"categories":[{"id":"1","value":"categoryValue1"},{"id":"2","value":"categoryValue2"}], "frequencies":[{"id":"1","value":"frequencyValue1"},{"id":"2","value":"frequencyValue2"}]
Building off the cityList example in the api, this is what I've got so far.
JsonRootObject interface:
public interface JsonRootObject {
List<SelectionProperties> getCategories();
List<SelectionProperties> getFrequencies();
}
JsonRootObjectAutoBeanFactory:
public interface JsonRootObjectAutoBeanFactory extends AutoBeanFactory {
AutoBean<JsonRootObject> jsonRootObject();
}
I created a SelectionProperties interface as all are single int/string value pairs:
public interface SelectionProperties {
String getId();
String getValue();
}
Now, according to the api:
// To convert from JSON data, extend a JsonReader and override
// createReturnData to return the desired type.
So I created a reader for both categories and frequencies;
CategoryReader:
public class CategoryReader
extends
JsonReader<ListLoadResult<SelectionProperties>, JsonRootObject> {
public CategoriesReader(AutoBeanFactory factory, Class<JsonRootObject> rootBeanType) {
super(factory, rootBeanType);
}
protected ListLoadResult<SelectionProperties> createReturnData(Object loadConfig, JsonRootObject incomingData) {
return new ListLoadResultBean<SelectionProperties>(incomingData.getCategories());
}
}
FrequencyReader:
public class FrequencyReader extends
JsonReader<ListLoadResult<SelectionProperties>, net.apoplectic.testapps.client.JsonRootObject> {
public FrequencyReader(AutoBeanFactory factory, Class<JsonRootObject> rootBeanType) {
super(factory, rootBeanType);
}
protected ListLoadResult<SelectionProperties> createReturnData(Object loadConfig, JsonRootObject incomingData) {
return new ListLoadResultBean<SelectionProperties>(incomingData.getFrequencies());
}
}
This doesn't feel quite right. I'm creating multiple instances of basically the same code and parsing the actual json string twice (at this point. I may have more options for the grid once I dig in). My question is am I missing something or is there a more efficient way to parse the response string? From my onSuccessfulResponse:
JsonRootObjectAutoBeanFactory factory = GWT.create(JsonRootObjectAutoBeanFactory.class);
CategoryReader catReader = new CategoriesReader(factory, JsonRootObject.class);
FrequencyReader freqReader = new FrequenciesReader(factory, JsonRootObject.class);
categories = catReader.read(null, response);
frequencies = freqReader.read(null, response);
List<SelectionProperties> categoriesList = categories.getData();
List<SelectionProperties> frequenciesList = frequencies.getData();
ListBox cateBox = new ListBox(false);
ListBox freqBox = new ListBox(false);
for (SelectionProperties category : categoriesList )
{
cateBox.addItem(category.getValue(), category.getId());
}
for (SelectionProperties frequency : frequenciesList)
{
freqBox.addItem(frequency.getValue(), frequency.getId());
}
The above does work, both the ListBoxes are populated correctly. I just wonder if this is the correct approach. Thanks in advance!