I am trying to map a Http JSON Response to a Custom Interface in Angular / typescript. I have tried it in several ways but have not made it yet. The JSON object is not correctly mapped to the interface. The map attribute stays "undefined". If I print the data directly, the JSON data is output correctly - the problem is that I don't know how to access it. Here is my code:
export interface IMap<T> {
map: Map<string, Array<T>>;
}
The JSON answer looks like this. It is Map< String,List< ? >> in Java.
{
"somenumbers": [
20,
40
],
"somemorenumbers": [
71,
111
]
}
Now I tried to map it the following way:
public getValues(
paramList: Array<string>
): Observable<IMap<any>> {
const url = `url`;
let params = new HttpParams();
for (let s of paramList) {
params = params.append("params", s);
}
return this.http.get<IMap<any>>(url, { params });
}
In the configservice I subscribe to the Method. How do I map the Response correctly so that the attribute map in data isn't undefined and can be accessed correctly?
this.configService
.getValues(["somenumbers", "somemorenumbers"])
.subscribe((data: IMap<any>) => {
//outputs the JSON Data as Object{somenumbers: Array(2), somemorenumbers: Array(2), map: Map(0)}
console.error(data);
console.error(data.map);//map is undefined => ERROR
});
As you can see the map attribute is undefined. It is just a "map: Map(0)". Now... - How do I get the JSON stuff into the export interface? The map attribute should be filled with the associated values.
I appreciate any help! :)
If I understood correctly you're expecting that by adding <IMap<any>> to the get call it will then return you the response mapped to IMap. It doesn't, check this issue.
What you can do instead is use rxjs map to map the response yourself like so:
return this.http.get<IMap<any>>(url, { params }).pipe(
map((response) => {
// map the response here
})
);
I realized that I actually don't need the export interface and changed the code to the following. It took a while for me to get that x.y is in ts the same as x["y"]. Via response[parameter] I can access the attributes of the response Object dynamically - exactly what I needed.
public getValues(
paramList: Array<string>
): Observable<Map<string, Array<any>>> {
const url = `url`;
let params = new HttpParams();
for (let s of paramList) {
params = params.append("params", s);
}
return this.http
.get<any>(url, {
params
})
.pipe(
map(response => {
let toReturn = new Map<string, any[]>();
for (let parameter of paramList) {
toReturn.set(parameter, response[parameter]);
}
return toReturn;
})
);
}
The mapping works now! The JSON answer is still the same as in the question above.
this.configService
.getValues(["somenumbers", "somemorenumbers"])
.subscribe((data: Map<string, any[]>) => {
console.error(data);
});
Thanks for the help and links #M Mansour !
Related
games json response
I originally had this piece of code
this.gameService.getAll()
.pipe(map(data => data['hydra:member']))
.subscribe( (games: Game[]) => this.games = games );
but if I want to retrieve more properties from the json response someone told me to do this
this.gameService.getAll()
.pipe(map(data => data['hydra:member']))
.subscribe(result => [result["hydra:member"], result['hydra:totalItems']]);
however how do you add the following to the above code:
(games: Game[]) => this.games = games );
and map totalItems to a variable.
with this line: pipe(map(data => data['hydra:member'])), it will only contain data from hydra:member.
Is this.games contain all data from API or the hydra:member?
try this:
this.gameService.getAll().subscribe((games: Game[]) => {
this.games = games;
this.totalItems = games['hydra:totalItems']
});
I would try defining my payload (the object I plan to return from the call) and fill that object out using type safety to help me out.
Something like this.
// you can just add more properties later if you like
export interface IGamesGetAllResponseObject {
games: any[]; // or better yet define the game object so it isn't any
totalGames: number;
}
// now define a function that will map the response to the intended object
export mapGamesGetAllToReponse(data): IGamesGetAllResponseObject {
return {
games: data['hydra:member'],
totalGames: data['hydra:totalItems'],
};
}
...
//then use it like this
this.gameService.getAll()
.pipe(map(mapGamesGetAllToReponse))
.subscribe( (result: IGamesGetAllResponseObject) => {
this.games = result.games;
this.totalGames = result.totalGames;
});
I haven't checked all my syntax there 100% but it should be very close to a solution for you.
I would also consider doing all that mapping in the service itself, rather than where you are subscribing. By doing that, you can contain the logic about what data you want and how to map it to the service, and the component just requests what it wants.
you can get multi property from the json response by call same property so that the above code will be :
this.gameService.getAll().pipe(map(result =>
{
this.games = result["hydra:member"] ;
this.totalItems = result['hydra:totalItems'];
}
)).subscribe();
and then call 'this.games' and 'this.totalItems' inline subscribe block.
I'll preface this saying I'm a UX Designer that's new to React.
I'm using Expo/React-Native and using the "Location" feature to get the device's location information. I'm able to get the information back in a JSON object by using:
const { height, width } = Dimensions.get("window");
class Explore extends Component {
static navigationOptions = {
header: null
};
state = {
locationResult: ""
};
componentDidMount() {
this._getLocationAsync();
}
_getLocationAsync = async () => {
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== "granted") {
this.setState({
locationResult: "Permission to access location was denied"
});
}
let location = await Location.getCurrentPositionAsync({});
let geocode = await Location.reverseGeocodeAsync(location.coords);
this.setState({ locationResult: JSON.stringify(geocode) });
};
And in my render I'm calling the value using:
<WelcomeText>
Discover {this.state.locationResult}
</WelcomeText>
Which returns the object:
[{"street":"Stockton St","city":"San Francisco","region":"CA","country":"United States","postalCode":"94108","isoCountryCode":"US","name":"1 Stockton St"}]
But how do I go about just displaying the value of "City" that's in the object?
Try this:
<WelcomeText>
Discover {this.state.locationResult[0].city}
</WelcomeText>
Explanation:
Your this.state.locationResult is an array of objects. With this.state.locationResult[0] we are accessing the first object. Then we can use the . operator to access the property we want. In your case .city
Edit:
You need to pass in geocode without stringifying it, otherwise you cannot access it like i described it above. reverseGeocodeAsync is already returning an array of objects, no need to transform it to a string.
Replace:
this.setState({ locationResult: JSON.stringify(geocode) });
with:
this.setState({ locationResult: geocode });
Edit2:
Here is a working example: https://snack.expo.io/B1SqSd3iN
I am using angular 4.2.6 for my application. I have a service like this
checkStaff(email: any) {
return this._http.post(this.url + "/Impsapi/getStaff", JSON.stringify(email)).map(
(resp) => resp
)
}
checkStaff(email:any){
return
this._http.post(this.url+"/Impsapi/getStaff",JSON.stringify(email)).map(
(resp)=> resp
)
}
this.loginServ.checkStaff(this.user)
.subscribe(
userData => {
this._return = userData;
console.log(this._return);
}
);
The Server returns JSON as response. but when i log the output, i get the below
logged response
please I need to consume the data in the body of the response. I have not been able convert the ._body to a proper json and use for the app. please help
The response data are in JSON string form. The app must parse that string into JavaScript objects by calling res.json().
return this._http.post(this.url + "/Impsapi/getStaff", JSON.stringify(email)).map(
(resp) => resp.json()
)
Update
try following code snippet
checkStaff(email: any) {
return this._http.post(this.url + "/Impsapi/getStaff", JSON.stringify(email))
.map(res => {return res.json()})
}
Try this:
this.loginServ.checkStaff(this.user)
.subscribe(
userData => {
this._return = userData.json();
console.log(this._return);
}
);
I mean your checkStaff:
checkStaff(email: any): Observable<Response> {
return this._http.post(this.url + "/Impsapi/getStaff", JSON.stringify(email));
}
export classMyResp
{
id: string;
/*so on...*/
}
This will give you the body of response If there is any.
I got my problem solved. My PHP is hosted on wampserver. In a way invalid JSON is always returned when i make call to the server. I had to use the ob_clean() function and everything is fine.
So I'm getting the following JSON structure from my asp.net core api:
{
"contentType": null,
"serializerSettings": null,
"statusCode": null,
"value": {
"productName": "Test",
"shortDescription": "Test 123",
"imageUri": "https://bla.com/bla",
"productCode": null,
"continuationToken": null
}
}
I have the following typescript function that invokes the API to get the above response:
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; //not working...
console.log("DATA: " + data);
console.log("DATA: " + data['value']);
return data;
});
}
ProductVM:
export interface ProductVM {
productName: string;
shortDescription: string;
imageUri: string;
productCode: string;
continuationToken: string;
}
My problem is that I can't deserialize it to ProductVM. The console logs just produce [object Object]
How can I actually map the contents of the value in my json response to a ProductVM object?
Is it wrong to say that data is a ProductVM in the map function? I have tried lots of different combinations but I cannot get it to work!
I'm unsure whether I can somehow automatically tell angular to map the value array in the json response to a ProductVM object or if I should provide a constructor to the ProductVM class (it's an interface right now), and extract the specific values in the json manually?
The data object in the map method chained to http is considered a Object typed object. This type does not have the value member that you need to access and therefore, the type checker is not happy with it.
Objects that are typed (that are not any) can only be assigned to untyped objects or objects of the exact same type. Here, your data is of type Object and cannot be assigned to another object of type ProductVM.
One solution to bypass type checking is to cast your data object to a any untyped object. This will allow access to any method or member just like plain old Javascript.
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: any) => this.externalProduct = data.value);
}
Another solution is to change your API so that data can deliver its content with data.json(). That way, you won't have to bypass type checking since the json() method returns an untyped value.
Be carefull though as your any object wil not have methods of the ProductVM if you ever add them in the future. You will need to manually create an instance with new ProductVM() and Object.assign on it to gain access to the methods.
From angular documentation: Typechecking http response
You have to set the type of returned data when using new httpClient ( since angular 4.3 ) => this.http.get<ProductVM>(...
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get<ProductVM>("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; // should be allowed by typescript now
return data;
});
}
thus typescript should leave you in peace
Have you tried to replace
this.externalProduct = data;
with
this.externalProduct = data.json();
Hope it helps
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map(data => {
this.externalProduct = <ProductVM>data;
console.log("DATA: " + this.externalProduct);
return data;
});
}
So, first we convert the response into a JSON.
I store it into response just to make it cleaner. Then, we have to navigate to value, because in your data value is the object that corresponds to ProductVM.
I would do it like this though:
Service
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get(`api/product?productCode=${code}`)
.map(data => <ProductVM>data)
.catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
Component
this.subscription = this.myService.getProductExternal(code).subscribe(
product => this.externalProduct = product,
error => console.warn(error)
);
I used this approach in a client which uses the method
HttpClient.get<GENERIC>(...).
Now it is working. Anyway, I do not understand, why I do not receive a type of T back from the http client, if I don't use the solution provided in the answer above.
Here is the client:
// get
get<T>(url: string, params?: [{key: string, value: string}]): Observable<T> {
var requestParams = new HttpParams()
if (params != undefined) {
for (var kvp of params) {
params.push(kvp);
}
}
return this.httpClient.get<T>(url, {
observe: 'body',
headers: this.authHeaders,
params: requestParams
}).pipe(
map(
res => <T>res
)
);
}
I realized a strange thing with my Angular2 typescript project. I have objects coming in from a webservice which have the type "Level" (it has the same properties as the Json coming from the webservice). In runtime comes out that the properties of the Level from the webservice have capital letters (Pascal case) at the beginning and the ones in my typescript project have small ones (visible in the browser's developer debug tool).
I guess I need to map the json properties somewhere somehow instead of doing a cast by writing "as Level[]" everywhere. How to I do it properly?
Update regarding the question that I should post some code:
(Controller)
ngOnInit(): void {
this.levelsObservable = this.levelsService.getAllLevels();
this.levelsObservable.subscribe(
data => console.log(data)
);
}
(Service)
observable : Observable<Response>;
getAllLevels(): Observable<Level[]> {
this.observable = this.achievementsService.getAllAchievements(this.allLevelsUrlPart);
return this.observable
.map((response: Response) => {
const srcData = response.json() as Level[];
return srcData;})
.catch(error => this.handleError(error));}
getAllAchievements(detailPath): Observable<Response> {
// prepare request url and header
this.specificUrl = this.webServiceUrl + detailPath;
this.headers.append('Content-type', 'application/json');
let options = new RequestOptions({ headers: this.headers });
this.result = this.http.get(this.specificUrl, options)
.catch(error => this.handleError(error));
return this.result;}
Update:
I polished my code a bit with the help of one answer below (not integrated above because not essential to solve the main problem).
I tried to use the other answer from below to reach the camel cases but it wasn't working (I have an array and in the array are objects with properties, but an object's properties aren't accessible with iterator methods).
Update:
I finally managed it (!) :) I shortened this post a bit and will now post my solution below. It's for sure not the most beautiful, but I'm happy to have one after searching around for hours. Thanks to all people helping me with their great and input here!
You could use this to get the lowercased objects.
modifiedSrc(srcData){
let obj = {};
Object.keys(srcData).forEach((key)=>{
obj[key.uncapitalize()] = srcData[key];
})
return obj
}
String.prototype.uncapitalize = function() {
return this.charAt(0).toLowerCase() + this.slice(1);
}
Then you can return the modified data
getAllLevels(): Observable<Level[]> {
this.observable = this.achievementsService.getAllAchievements(this.allLevelsUrlPart);
return this.observable
.map((response: Response) => {
const srcData = response.json() as Level[];
return this.modifiedSrc(srcData);})
.catch(error => this.handleError(error));}
You have complicated both of your methods.Make it simple as
this.webServiceUrl = "http...." ; // your service end point address
this.headers.append('Content-type', 'application/json');
let options = new RequestOptions({ headers: this.headers });
// For all your error handling
private handleError(error: Response) {
console.log(error);
return Observable.throw(error.json().error || 'Internal Server error');
}
Your service method can use TypeCasting which will look like
getAllLevels(detailPath): Observable<Level[]> {
return this.http.get(detailPath, options)
.map((response: Response) => <Level[]>response.json())
.catch(this.handleError);
}
Your component should raise the request to your service as
ngOnInit() : void{
this._myService.getAllLevels()
.subscribe(levels => this.levels = levels,
error =>this.errorMessage =<any> error);
}
Your variable declaration must be like
levels:Level[];
So finally... I got a solution! For sure not the most beautiful one, but easy to understand and done with hard work and research:
private useLevelProperties (response: any): Level[]{
let levels: Level[] = [];
Object.keys(response).forEach((key) => {
//create a new object and just take out the json parts needed. The webservice retrieves Pascal case letters, so we
//need to convert them into camel case ones.
this.level = new Level(response[key]["AchievementId"], response[key]["Image"],
response[key]["GrantedTo"], response[key]["GrantedBy"], response[key]["GrantedWhen"], response[key]["Description"],
response[key]["Name"], response[key]["CompetitionCode"], response[key]["Number"]);
levels[key] = this.level;
});
return levels;
};
Another option is to do it server-side with an extra json option for camel case: example for server-side camel casing