Max tools & json data : .net or max script?

So our game is data driven by json files.

I need to write MAX tools that will edit and update bits of json data.

I see two basic approaches available:
1.) use max script to read/write files, for which I’ll need to write some parsing functions
or
2.) use .NET, in which case I’ll need to bone up on that stuff -I’ve never used .NET objects in Max scripts

Has anyone dealt with JSON in max script? how did you go about it?

If you search for xmlOps on this forum you should find some code for working with XML via .NET in MXS. You can easily take that same pattern and map it to using a .NET JSON library. Either way, just learning how to use .NET through MXS is an essential skill. Under no circumstances should you try to write your own JSON loader/dumper for MXS.

I’ve been pursuing JSON.NET as a possible solution but I am ending up with data that is

Maxx script wrapping dotnet objects wrapping LINQ objects wrapping json data.

trying to get the keys and values out is getting to be a headache, and I am still not convinced I can really use it.

boy I miss python…

follow the CGtalk threadfor details

oh how I know what you mean.
it’s been a year now we have transitioned our pipeline (after a change of engine) over from Maya to Max, and it’s still so painful.
At least we have the chance (?) of using 2013, so I end up most of the time rolling out full .net max plugins just to parse various config files and such. I then just return some List or HashSet to maxscript and work from there.
It could actually be quite enjoyable to keep doing .net plugins for Max if only they had a decent .net sdk. As it stands right now, it’s just so shitty it’s unthinkable they even decided to release it officially.
I have no idea what Max 2014 has in store for us, but I hope they restarted their .net sdk from scratch. That’s how bad it is.

I have been thinking the best approach would be to parse the json entirely in a c# class that also passes and receives more max script-friendly data…but that’s a bit past my C# skills today.

i noticed that on autodesk’s 3dsmax user feedback page for scripting, python is the #1 request by a mile…
If they restart their sdk form scratch maybe they should implement some python :slight_smile:

I am making some useful progress with json.net, I’ll post more info after the deadline…

Good to hear. Curious to see how it’s working out for you.

I was looking at it the other day (as I’m a Maya guy working at a largely Max studio now), but didn’t get a chance to set it up.

Thats another option for JSON, as Python has at least 3 billion libraries for doing that. :slight_smile:

I’ve condensed what I’ve learned into on giant chunk of heavily commented code, below.

The basic idea is that we have MXS wrapping dotNet objects, these objects hold and manipulate Json data.

Whats really going on is MXS wraps dotNetObject wrapping LINQ objects Wrapping Json data -but thinking about it to hard complicates usage.

All we care about are 4 basic dotNet objects : JObject, JArray, Jproperty and JValue, and the properties and methods for their use. Briefly:

[I]Newtonsoft.Json.Linq.JObject[/I] contains {< anything >} as its [I].value[/I] property, plus has methods and properties for manipulation.

[I]Newtonsoft.Json.Linq.JArray[/I] contains[ any array] as its [I].value[/I] property, plus has methods and properties for manipulation.

[I]...JProperty[/I] contains a single  "<Key>":<value> pair, plus has methods and properties for manipulation."<key>" is the [I].name [/I]property, <value> is  its [I].value.value [/I] property 
(that's not a typo, it is .value.vlaue...see the code comments)

[I]...JValue[/I] contains a single json value < string, number, boolean, JOBject or JArray), as its [I].value[/I] property plus has methods and properties for manipulation.

they are described breifly in the comments below, and with technical thouroughness in the JSON.NET documentation
http://james.newtonking.com/projects/json/help/

/* 
First, some max script to read json files as strings.
*/

testFilePath="C:/path/to/my/jsonData.json"
/*
note the slash " / " instead of windows back slash.
this is just personal choice, to avoid using the double back slash 
necessary to correctly escape the path delimiters
"C:\\path\	o\\my\\jsonData.json" is equally valid.
*/

fn getJsonFileAsString filePath=(
	local  jsonString=""
	fs=openFile filePath
	while not eof fs do(
		jsonString += readchar fs
	)
	close fs
	return jsonString
)

jsonString= getJsonFileAsString testFilePath

/*
Now we're ready for some action.
download the JSON.NET library from http://json.codeplex.com/
and copy 
	..\Json45r11\Bin\Net40\Newtonsoft.Json.dll
	..\Json45r11\Bin\Net40Newtonsoft.Json.xml (not sure if this is necessary)
into 
	C:\Program Files\Autodesk\3ds Max 2013\
this will put the library where Max can see it.
*/

--use dotNet.loadAssembly to load the Newtonsoft.Json library:

json = dotNet.loadAssembly  "Newtonsoft.Json.dll"

--#> dotNetObject:System.Reflection.RuntimeAssembly

/*

a look at http://json.codeplex.com/documentation reveals that most of the functions 
are for loading JSON into a pre-defined C# object 
whose keys and structure match *exactly* the JSON you are reading.
This is not a very flexible approach. 

We want a dotNetObject that we can fully query and manipulate.
JSON.NET utilizes LINQ objects for this. 

the class Newtonsoft.Json.Linq.JObject wraps json data in a LINQ object.

But using MaxScript wrapping DotNet Wrapping LINQ Wrapping JSON gets confusing fast!
It's challenging to excavate the acutal JSON data form all these wrappers.
Lets see if we can dig out useful json data...

*/

--first, instantiate a dotNetObject using the class Newtonsoft.Json.Linq.JObject. This creates an empty JObject.
myJObject=dotNetObject "Newtonsoft.Json.Linq.JObject"
--#> dotNetObject:Newtonsoft.Json.Linq.JObject

--next, use the Newtonsoft.Json.Linq.JObject.parse method to read our json string into the JObject we just made
myJObject=myJObject.parse jsonString
--#> dotNetObject:Newtonsoft.Json.Linq.JObject
--the method myJObject.parse has populated our JObject with the data in the string.

--get the first json object in myJObject
myFirstItem=myJObject.First
--#> dotNetObject:Newtonsoft.Json.Linq.JValue

--get the value of myFirstItem
myFirstItem.Value
--#>dotNetObject:Newtonsoft.Json.Linq.JValue

/*
hey, that's not a json value!
It's a dotNetObject of the Class JValue. It holds a JSON value, 
along with some useful properties and methods for manipulating and searching json data.
*/

myFirstItem.Value.Value
--#> "something" 

/*
sweet! first object's value is revelaed to max script. how about the key?
the creator of JSON.NET himself posted this C# function to generate a list of keys:
	IList<string> keys = parent.Properties().Select(p => p.Name).ToList();
It won't work in Max script, of course. But it implies that keys are the .Name property of myFirstItem.value's .Parent property:
*/

myFirstItem.Value.Parent.Name
--#> "key"
-- same as :
myFirstItem.Name
--#> "key"
/*
cool, we can read the first ojects name and key.
what if we want to refer to the key and get the value?
*/
myJObject.item["key"].value

/*
THE JSON.NET OBJECT ZOO

so we have a small zoo of dotNetObjects to hold and manipulate json data.
The four basic types 

JObject : JSON.NET class; a dotNetObject that represents a json object -anything between {} . Not actual JSON data, 
	but a container with its own properties and methods.

JArray : JSON.NET class; a dotNetObject that represents a json Array -anythign between []. Specifically, an array of 
	Jobjects, JArrays, JProperties, Jvalues.

JProperty: JSON.NET class; a dotNetObject that represents a Key:Value Pair.

JValue: JSON.NET class; a dotNetObject container for a value in JSON (string, integer, date, etc.) 
	*Not actual JSON data*, but a container with its own props and methods.
	MOST CONFUSINGLY: JValue.value might retrun a simple  value (string, number, boolean)  OR any of the above objects.

lets see what these dotNetObjects can do for us...
*/

myJObject=myJObject.parse jsonString
--#> dotNetObject:Newtonsoft.Json.Linq.JObject 
--returns a JObject : holds json data

myFirstItem=myJObject.First 
--#> dotNetObject:Newtonsoft.Json.Linq.JProperty
--returns a JProperty : a key:value pair

myFirstItem.name
--#>"key"
--the key of the JProperty

myFirstItem.value --JValue object.
--#>dotNetObject:Newtonsoft.Json.Linq.JValue
--returns a JValue : the value portion of a key value pair.
--note this is not the actual value, but a dotNetObject for holding and manipulating the value

--reading a JSON value:
myFirstItem.value.value 
--#>"myValue"
--JValue.value property; retruns the actual Json VALUE

--writing a JSON value:
myFirstItem.value.value = "newValue"
--#>"newValue"
--we set a new value for "key" 


/*
Json arrays...
*/

myJArray=myJObject.Item["myJsonArray"]
--#>dotNetObject:Newtonsoft.Json.Linq.JArray
myJArray.item(0)
--#>dotNetObject:Newtonsoft.Json.Linq.JObject

/*
so myJArray.item[0] appears to contain a Json object
JArray.item() access is a dotnet function / method, using zero indexing,
so the usual mxs Array access  with brackets and 1 indexing - "myArray[1]"-  won't work.
the fact that we have to use () to access JArray elements creates some scoping issues in mxs.
lets take a look:
*/

--make a simple json array key:value pair, wrapped in a json object as a string
myJarray_string= "{myArray:[\"foo\",\"bar\", \"baz\"]}"
--#> "{myArray:["foo","bar", "baz"]}"

--instantiate a JObject, parse the string into the JObject
myJarray = dotNetObject "Newtonsoft.Json.Linq.JObject"
--#>dotNetObject:Newtonsoft.Json.Linq.JObject
myJarray = myJarray.Parse myJarray_string
--#>dotNetObject:Newtonsoft.Json.Linq.JObject


myJarray.item["myArray"]
--#>dotNetObject:Newtonsoft.Json.Linq.JArray 
--the value of key "myArray" is a JArray object

myJarray.item["myArray"].item(0)
--#>dotNetObject:Newtonsoft.Json.Linq.JValue
-- myArray[1] is a JValue object, so we * should * be able to use .value to see the value...

myJarray.item["myArray"].item(0).value
--#>-- Error occurred in anonymous codeblock; filename: C:\Program Files\Autodesk\3ds Max 2013\scripts\; position: 633; line: 19
--#>-- Unknown property: "value" in 0
/*
uh oh, somethign hinky is going on. mxs is looking for "0.value" instead of "item(0).value".
it seems the () cause a scoping problem. 
since <myJarray.item["myArray"].item(0) > really represents a single variable
we can fix this by wrapping the whole thing in another set of ():
*/
(myJarray.item["myArray"].item(0)).value
--#>"foo"

--although it maight be neater to just sue a variable
item1=myJarray.item["myArray"].item(0)
item1.value


/*
exposing more JSON.NET properties:
below is a function to expose the values of properties of JSON.NET JObjects and JObject subclasses
to use: 
	1.) instantiate a JObject
		myJObject=dotNetObject
		--#>"Newtonsoft.Json.Linq.JObject"
	2.) populate it by parseing  a JSON string
		myJObject=o.parse < a Json String >
	3.) pass *as a string*  when calling the function
		exposeProps "myJObject" 
		(NOT: exposeProps myJObject)
*/

fn exposeProps objString =(

	obj=execute(objString)--evaluate string as actual object
	for propName in (getPropNames obj) do (
		p=propName as string
		format "% % : %
" objString p (execute(objString+"."+p))
	)
)


/* 
recursing unknown JSON
the above is all well and good if we know the JSON data we want to mess with.
but what if the JSON data is variable, or unkown?
below is a fucntion to recurse an unknown json object and pretty print its contents.
*/

--tab fucntions to format output
tab=""
fn tabIn=(tab += "	")
fn tabOut=(	try(tab = replace  tab  1 1 "")catch())

--recursing unkown JSON
fn recurseJData jdata=(
	tabIn()
	--format "<%>" data
	local data=jdata
	local typeIndex=data.type.value__
	local dType=JTokenTypes[typeIndex+1]

	case data.hasValues of (
		true:(
			--data contains values
			--action: deteemine if JProperty, object or array

				case data.type.value__ of (
					
					4:(--data is type JProperty ( a key:value pair )
						--action: print key, recurse value
						--you could begin storing the keys in a Maxscript array or whatever
	
						local key=data.name
						local val=data.value
						format "
%%:"tab key
						recurseJData val
					
					)
					
					1: (
						--Data is a Json Object
						--tabIn()
						format "
%{" tab
						local oc=0
						local objData=data.first
						for oc=1 to data.count do(
							if objData == undefined do(format "  ERROR: obj:  %" objdata; continue)
							recurseJData(objData)
							--format "%NEXT:%" tab childData.next
							objData=objData.next
							
						)
						oc=0
						format "
%}" tab
						--tabOut()
					)
					
					2: (--Data is a Json Array
						--tabIn()
						format "
%[" tab
						local jArray=data					
						for i=1 to jArray.count do(
							
							element=jArray.item(i-1)
							--if element == undefined do(format " !!!%" elementData; continue)
							recurseJData(element)
							
						)
						--tabOut()
						format "
%]" tab
						
						
					)										
				)
			
		)
		
		false:(
			--data IS a value
			--action: print value
			case data.type.value__ of (
				6:(--integer
					format " %"data.value
					)
				7:(--float
					format " %"data.value
					)
				8:(--string
					format "\"%\""data.value 
					)
				9:(--boolean
					format " %"data.value
					)
				default: (
					format "% WARNING: unexpected JTokenType (%-%) in {%}" tab typeIndex dType currentItem
				)	
				
			)
			
		)
	)
	tabOut()
)


/*

and finaly, once we are done with or JSON object and wish to write it to a file,
we can use the JObject.toString() method to gnerate a string fo json
and then use mxs format <string> to: <stream> to write the json file
*/
fn writeJsonObjectToFile jsonObject filePath =(
	format " SAVE: %
" filePath 
	local jsonString=jsonObject.toString()
	print jsonString--just to verify in listener
	--open filepath as stream, use a writable mode
	fs= openFile filePath mode:"wt" --open specified json file
	format "%" jsonString to:fs--write string to the file
	close fs
	format " SAVED: %
" filePath --save
	)

Some of this is doubtless sloppy code, but I did get it working for my purposes.

What I’s really love to make is some kind of dynamic, recursive native mxs object for json data, so lines like :
myJObject.item[“key”].value.value=“foo” could be replaced by a simpler
myJObject.key=“foo”

structs , which I’m admittedly not too farmiliar with, seem too static and non recursive form the examples…

[update 3.20.2013] found some scoping issues with JArray syntax, updated code and comments.

Hi and thanks for setting this up! I’ve been setting up a data-driven system using Excel and OLE Automation. It’s been useful but very touchy so I’d rather use something more straight-forawrd.

So using JSON with MXS is a great start.

When I run the code above with a JSON file of my own, I get an error referencing this line (65):

myJObject=myJObject.parse jsonString

– Runtime error: dotNet runtime exception: Bad JSON escape sequence:

It’s almost as if the property “parse” does not exist in this context. Does anyone know why this error occurs? I’m using 3ds Max 2016 SP2 on Windows 7 x64.

Also, Mambo4, in the years since you have posted this, have you discovered better methods for retrieving JSON data? I’ll continue looking for solutions to this. Thanks!

Chances are “parse” should should be capitalized

myJObject=myJObject.Parse jsonString

I would not use the code above, it was my first attempt.
I did further work on an mxs json wrapper struct and put it up onmy code page here
It’s a little cleaner code -wise.

inspired by this and other recent posts, I have just updated it.
provided as-is, no returns :slight_smile: